From 07b489325278fbd4673b755c83f5eb84cd51c33f Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sun, 24 May 2026 13:55:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(llmage):=20=E6=B7=BB=E5=8A=A0llmusage?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95=E5=A4=87=E4=BB=BD=E5=92=8C?= =?UTF-8?q?=E8=AE=B0=E8=B4=A6=E5=A4=B1=E8=B4=A5=E6=A3=80=E7=B4=A2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 llmusage_history 表:定时备份已记账(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) diff --git a/llmage/init.py b/llmage/init.py index 5c7471c..e0f3080 100644 --- a/llmage/init.py +++ b/llmage/init.py @@ -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) diff --git a/models/llmusage_accounting_failed.json b/models/llmusage_accounting_failed.json new file mode 100644 index 0000000..554cc3f --- /dev/null +++ b/models/llmusage_accounting_failed.json @@ -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"] + } + ] +} diff --git a/models/llmusage_history.json b/models/llmusage_history.json new file mode 100644 index 0000000..1ad6e29 --- /dev/null +++ b/models/llmusage_history.json @@ -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"] + } + ] +} diff --git a/scripts/migrate_llmusage_history.sql b/scripts/migrate_llmusage_history.sql new file mode 100644 index 0000000..5c30e0c --- /dev/null +++ b/scripts/migrate_llmusage_history.sql @@ -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; +-- ============================================================ diff --git a/scripts/setup_llmage_perms.sh b/scripts/setup_llmage_perms.sh index aaf4e63..20b7329 100644 --- a/scripts/setup_llmage_perms.sh +++ b/scripts/setup_llmage_perms.sh @@ -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} 条权限" diff --git a/wwwroot/api/failed_accounting_list.dspy b/wwwroot/api/failed_accounting_list.dspy new file mode 100644 index 0000000..ec7af7d --- /dev/null +++ b/wwwroot/api/failed_accounting_list.dspy @@ -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) diff --git a/wwwroot/api/llmusage_accounting_failed_create.dspy b/wwwroot/api/llmusage_accounting_failed_create.dspy new file mode 100644 index 0000000..ae87374 --- /dev/null +++ b/wwwroot/api/llmusage_accounting_failed_create.dspy @@ -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) diff --git a/wwwroot/api/llmusage_accounting_failed_delete.dspy b/wwwroot/api/llmusage_accounting_failed_delete.dspy new file mode 100644 index 0000000..2a09928 --- /dev/null +++ b/wwwroot/api/llmusage_accounting_failed_delete.dspy @@ -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) diff --git a/wwwroot/api/llmusage_accounting_failed_update.dspy b/wwwroot/api/llmusage_accounting_failed_update.dspy new file mode 100644 index 0000000..3ce34a1 --- /dev/null +++ b/wwwroot/api/llmusage_accounting_failed_update.dspy @@ -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) diff --git a/wwwroot/api/llmusage_create.dspy b/wwwroot/api/llmusage_create.dspy new file mode 100644 index 0000000..466e806 --- /dev/null +++ b/wwwroot/api/llmusage_create.dspy @@ -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) diff --git a/wwwroot/api/llmusage_delete.dspy b/wwwroot/api/llmusage_delete.dspy new file mode 100644 index 0000000..58cb1f1 --- /dev/null +++ b/wwwroot/api/llmusage_delete.dspy @@ -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) diff --git a/wwwroot/api/llmusage_update.dspy b/wwwroot/api/llmusage_update.dspy new file mode 100644 index 0000000..5d77ba8 --- /dev/null +++ b/wwwroot/api/llmusage_update.dspy @@ -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) diff --git a/wwwroot/failed_accounting.ui b/wwwroot/failed_accounting.ui new file mode 100644 index 0000000..b4016a8 --- /dev/null +++ b/wwwroot/failed_accounting.ui @@ -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'?'已处理':'未处理';}"} + ] + } + } + ] +} diff --git a/wwwroot/index.ui b/wwwroot/index.ui index f024883..4123f00 100644 --- a/wwwroot/index.ui +++ b/wwwroot/index.ui @@ -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": "", + "width": "40px", + "height": "40px" + } + }, + { + "widgettype": "Title4", + "options": { + "text": "记账失败记录", + "color": "#ffffff", + "marginTop": "12px" + } + }, + { + "widgettype": "Text", + "options": { + "text": "查看和检索记账失败的记录", + "color": "#ef5350", + "fontSize": "14px" + } + } + ] } ] },