From 054b0dd8e2839520510a9fabc7d98a526d6ffb7c Mon Sep 17 00:00:00 2001 From: yumoqing Date: Fri, 17 Jul 2026 18:11:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20unified=20dashboard=20=E2=80=94=20all?= =?UTF-8?q?=20dashboard=5Ffor=5Fsage=20metrics=20from=20data=20mart,=206?= =?UTF-8?q?=20stat=20cards,=20perf+ops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sage_datamark/dashboards.py | 136 ++++++++++++++++++++++ sage_datamark/init.py | 119 +++++++++++++++----- scripts/load_path.py | 3 + wwwroot/api/customer_models.dspy | 4 + wwwroot/api/daily_trend.dspy | 4 + wwwroot/api/hourly_concurrency.dspy | 4 + wwwroot/api/stats.dspy | 4 + wwwroot/api/top_models.dspy | 4 + wwwroot/api/top_providers.dspy | 4 + wwwroot/api/top_users.dspy | 4 + wwwroot/index.ui | 168 +++++++++++++++++++++++----- wwwroot/stat_success_rate.ui | 11 +- wwwroot/stat_today_amount.ui | 9 ++ wwwroot/stat_today_usage.ui | 9 ++ 14 files changed, 417 insertions(+), 66 deletions(-) create mode 100644 sage_datamark/dashboards.py create mode 100644 wwwroot/api/customer_models.dspy create mode 100644 wwwroot/api/daily_trend.dspy create mode 100644 wwwroot/api/hourly_concurrency.dspy create mode 100644 wwwroot/api/stats.dspy create mode 100644 wwwroot/api/top_models.dspy create mode 100644 wwwroot/api/top_providers.dspy create mode 100644 wwwroot/api/top_users.dspy create mode 100644 wwwroot/stat_today_amount.ui create mode 100644 wwwroot/stat_today_usage.ui diff --git a/sage_datamark/dashboards.py b/sage_datamark/dashboards.py new file mode 100644 index 0000000..076103e --- /dev/null +++ b/sage_datamark/dashboards.py @@ -0,0 +1,136 @@ +""" +统一 Dashboard API — 从集市表查询所有运营指标 + 性能指标 +替代 dashboard_for_sage 中对 llmusage 的直接查询 +""" +from sqlor.dbpools import get_sor_context +from datetime import datetime, timedelta + + +# ── 统计卡片 ── + +async def get_today_usage(env, sor): + """当天调用总数""" + sql = "SELECT COUNT(*) as cnt FROM dm_model_call_fact WHERE call_date = ${today}$" + recs = await sor.sqlExe(sql, {'today': env.curDateString()}) + return int(recs[0].cnt) if recs else 0 + + +async def get_today_amount(env, sor): + """当天总金额""" + sql = "SELECT COALESCE(SUM(amount), 0) as total FROM dm_model_call_fact WHERE call_date = ${today}$" + recs = await sor.sqlExe(sql, {'today': env.curDateString()}) + return round(float(recs[0].total), 2) if recs else 0 + + +async def get_success_rate(env, sor): + """当天成功率""" + sql = """SELECT + COUNT(*) as total, + SUM(CASE WHEN status='SUCCEEDED' THEN 1 ELSE 0 END) as success + FROM dm_model_call_fact WHERE call_date = ${today}$""" + recs = await sor.sqlExe(sql, {'today': env.curDateString()}) + if recs and recs[0].total: + return round(recs[0].success * 100.0 / recs[0].total, 1) + return 0 + + +async def get_fail_count(env, sor): + """当天失败数""" + sql = "SELECT COUNT(*) as cnt FROM dm_model_call_fact WHERE call_date = ${today}$ AND status = 'FAILED'" + recs = await sor.sqlExe(sql, {'today': env.curDateString()}) + return int(recs[0].cnt) if recs else 0 + + +async def get_active_users(env, sor): + """当天活跃用户数""" + sql = "SELECT COUNT(DISTINCT userid) as cnt FROM dm_model_call_fact WHERE call_date = ${today}$" + recs = await sor.sqlExe(sql, {'today': env.curDateString()}) + return int(recs[0].cnt) if recs else 0 + + +# ── 排行榜 ── + +async def get_top_models(env, sor, limit=5): + """当天 Top N 模型(按调用次数)""" + sql = """SELECT model, COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount + FROM dm_model_call_fact WHERE call_date = ${today}$ + GROUP BY model ORDER BY cnt DESC LIMIT ${limit}$""" + recs = await sor.sqlExe(sql, {'today': env.curDateString(), 'limit': str(limit)}) + return [{'model_name': r.model or 'Unknown', 'cnt': int(r.cnt), + 'total_amount': round(float(r.total_amount), 2)} for r in recs] + + +async def get_top_providers(env, sor, limit=5): + """当天 Top N 供应商(按金额)""" + sql = """SELECT providerid, COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount + FROM dm_model_call_fact WHERE call_date = ${today}$ AND providerid IS NOT NULL + GROUP BY providerid ORDER BY total_amount DESC LIMIT ${limit}$""" + recs = await sor.sqlExe(sql, {'today': env.curDateString(), 'limit': str(limit)}) + return [{'provider_name': r.providerid or 'Unknown', 'cnt': int(r.cnt), + 'total_amount': round(float(r.total_amount), 2)} for r in recs] + + +async def get_top_users(env, sor, limit=5): + """全量 Top N 用户(按金额)""" + sql = """SELECT userid, COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount + FROM dm_model_call_fact GROUP BY userid ORDER BY total_amount DESC LIMIT ${limit}$""" + recs = await sor.sqlExe(sql, {'limit': str(limit)}) + return [{'user_name': r.userid, 'cnt': int(r.cnt), + 'total_amount': round(float(r.total_amount), 2)} for r in recs] + + +# ── 客户维度 ── + +async def get_customer_daily_models(env, sor, userorgid): + """客户当天各模型用量""" + sql = """SELECT model, COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount + FROM dm_model_call_fact WHERE call_date = ${today}$ AND userorgid = ${orgid}$ + GROUP BY model ORDER BY cnt DESC""" + recs = await sor.sqlExe(sql, {'today': env.curDateString(), 'orgid': userorgid}) + return [{'model_name': r.model or 'Unknown', 'cnt': int(r.cnt), + 'total_amount': round(float(r.total_amount), 4)} for r in recs] + + +async def get_customer_monthly_models(env, sor, userorgid): + """客户当月各模型用量""" + now = datetime.now() + month_start = now.strftime('%Y-%m-01') + sql = """SELECT model, COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount + FROM dm_model_call_fact WHERE call_date >= ${start}$ AND userorgid = ${orgid}$ + GROUP BY model ORDER BY cnt DESC""" + recs = await sor.sqlExe(sql, {'start': month_start, 'orgid': userorgid}) + return [{'model_name': r.model or 'Unknown', 'cnt': int(r.cnt), + 'total_amount': round(float(r.total_amount), 4)} for r in recs] + + +async def get_customer_user_today(env, sor, userorgid): + """客户当天各用户用量""" + sql = """SELECT userid, COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount + FROM dm_model_call_fact WHERE call_date = ${today}$ AND userorgid = ${orgid}$ + GROUP BY userid ORDER BY cnt DESC""" + recs = await sor.sqlExe(sql, {'today': env.curDateString(), 'orgid': userorgid}) + return [{'user_name': r.userid, 'cnt': int(r.cnt), + 'total_amount': round(float(r.total_amount), 4)} for r in recs] + + +# ── 趋势 ── + +async def get_daily_trend(env, sor, days=7): + """最近 N 天调用量和金额趋势""" + sql = """SELECT call_date, COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount + FROM dm_model_call_fact + WHERE call_date >= ${start}$ + GROUP BY call_date ORDER BY call_date""" + start = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d') + recs = await sor.sqlExe(sql, {'start': start}) + return [{'date': r.call_date, 'cnt': int(r.cnt), + 'amount': round(float(r.total_amount), 2)} for r in recs] + + +async def get_hourly_concurrency(env, sor): + """当天每小时并发(活跃用户)""" + sql = """SELECT call_hour, COUNT(DISTINCT userid) as users, COUNT(*) as calls + FROM dm_model_call_fact WHERE call_date = ${today}$ + GROUP BY call_hour ORDER BY call_hour""" + recs = await sor.sqlExe(sql, {'today': env.curDateString()}) + return [{'hour': r.call_hour, 'users': int(r.users), 'calls': int(r.calls)} for r in recs] diff --git a/sage_datamark/init.py b/sage_datamark/init.py index e3fbc9c..178e958 100644 --- a/sage_datamark/init.py +++ b/sage_datamark/init.py @@ -1,23 +1,24 @@ -"""sage_datamark 模块初始化""" +"""sage_datamark 模块初始化 — 统一 Dashboard API""" from ahserver.serverenv import ServerEnv from appPublic.jsonConfig import getConfig from appPublic.log import debug, exception -from sqlor.dbpools import DBPools +from sqlor.dbpools import DBPools, get_sor_context from .etl import sync_call_fact, aggregate_daily_perf, aggregate_provider_cost, ensure_tables +from .dashboards import ( + get_today_usage, get_today_amount, get_success_rate, get_fail_count, + get_active_users, get_top_models, get_top_providers, get_top_users, + get_customer_daily_models, get_customer_monthly_models, + get_customer_user_today, get_daily_trend, get_hourly_concurrency, +) MODULE_NAME = "sage_datamark" -def get_dbname(): - env = ServerEnv() - return env.get_module_dbname(MODULE_NAME) - +# ── 性能指标 API ── async def api_model_perf(sor, params_kw=None): - """模型性能统计 API — POST /sage_datamark/api/model_perf.dspy""" model = params_kw.get('model') if params_kw else None stat_date = params_kw.get('date') if params_kw else None - sql = "SELECT * FROM dm_model_perf_daily WHERE 1=1" ns = {} if stat_date: @@ -27,16 +28,13 @@ async def api_model_perf(sor, params_kw=None): sql += " AND model=${model}$" ns['model'] = model sql += " ORDER BY total_calls DESC LIMIT 50" - rows = await sor.sqlExe(sql, ns) return [dict(r) for r in rows] async def api_provider_roi(sor, params_kw=None): - """供应商性价比 API — POST /sage_datamark/api/provider_roi.dspy""" model = params_kw.get('model') if params_kw else None stat_date = params_kw.get('date') if params_kw else None - sql = "SELECT * FROM dm_provider_cost_daily WHERE 1=1" ns = {} if stat_date: @@ -46,33 +44,96 @@ async def api_provider_roi(sor, params_kw=None): sql += " AND model=${model}$" ns['model'] = model sql += " ORDER BY total_amount DESC LIMIT 50" - rows = await sor.sqlExe(sql, ns) return [dict(r) for r in rows] -async def api_call_detail(sor, params_kw=None): - """调用明细 API — POST /sage_datamark/api/call_detail.dspy""" - stat_date = (params_kw or {}).get('date') - userorgid = (params_kw or {}).get('userorgid') +# ── 运营指标 API(替代 dashboard_for_sage)── - sql = "SELECT * FROM dm_model_call_fact WHERE 1=1" - ns = {} - if stat_date: - sql += " AND call_date=${stat_date}$" - ns['stat_date'] = stat_date - if userorgid: - sql += " AND userorgid=${userorgid}$" - ns['userorgid'] = userorgid - sql += " ORDER BY call_time DESC LIMIT 200" +async def api_stats(request): + """统计卡片数据 — /sage_datamark/api/stats.dspy""" + env = request._run_ns + async with get_sor_context(env, MODULE_NAME) as sor: + return { + 'today_usage': await get_today_usage(env, sor), + 'today_amount': await get_today_amount(env, sor), + 'success_rate': await get_success_rate(env, sor), + 'fail_count': await get_fail_count(env, sor), + 'active_users': await get_active_users(env, sor), + } - rows = await sor.sqlExe(sql, ns) - return [dict(r) for r in rows] + +async def api_top_models(request): + """Top 模型 — /sage_datamark/api/top_models.dspy""" + env = request._run_ns + async with get_sor_context(env, MODULE_NAME) as sor: + return await get_top_models(env, sor) + + +async def api_top_providers(request): + """Top 供应商 — /sage_datamark/api/top_providers.dspy""" + env = request._run_ns + async with get_sor_context(env, MODULE_NAME) as sor: + return await get_top_providers(env, sor) + + +async def api_top_users(request): + """Top 用户 — /sage_datamark/api/top_users.dspy""" + env = request._run_ns + async with get_sor_context(env, MODULE_NAME) as sor: + return await get_top_users(env, sor) + + +async def api_customer_models(request): + """客户用量 — /sage_datamark/api/customer_models.dspy""" + env = request._run_ns + userorgid = await env.get_userorgid() + async with get_sor_context(env, MODULE_NAME) as sor: + return { + 'daily': await get_customer_daily_models(env, sor, userorgid), + 'monthly': await get_customer_monthly_models(env, sor, userorgid), + } + + +async def api_daily_trend(request): + """7天趋势 — /sage_datamark/api/daily_trend.dspy""" + env = request._run_ns + async with get_sor_context(env, MODULE_NAME) as sor: + return await get_daily_trend(env, sor) + + +async def api_hourly_concurrency(request): + """每小时并发 — /sage_datamark/api/hourly_concurrency.dspy""" + env = request._run_ns + async with get_sor_context(env, MODULE_NAME) as sor: + return await get_hourly_concurrency(env, sor) + + +# ── Jinja2 模板函数(给 stat_*.ui 调用)── + +async def j2_today_usage(request): + env = request._run_ns + async with get_sor_context(env, MODULE_NAME) as sor: + return await get_today_usage(env, sor) + + +async def j2_today_amount(request): + env = request._run_ns + async with get_sor_context(env, MODULE_NAME) as sor: + return await get_today_amount(env, sor) def load_sage_datamark(): env = ServerEnv() env.api_model_perf = api_model_perf env.api_provider_roi = api_provider_roi - env.api_call_detail = api_call_detail - debug("[sage_datamark] registered API endpoints") + env.api_stats = api_stats + env.api_top_models = api_top_models + env.api_top_providers = api_top_providers + env.api_top_users = api_top_users + env.api_customer_models = api_customer_models + env.api_daily_trend = api_daily_trend + env.api_hourly_concurrency = api_hourly_concurrency + env.j2_today_usage = j2_today_usage + env.j2_today_amount = j2_today_amount + debug("[sage_datamark] registered all API endpoints") diff --git a/scripts/load_path.py b/scripts/load_path.py index cde76dd..a3592c4 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -11,10 +11,13 @@ PATHS_LOGINED = [ f"/{MOD}", f"/{MOD}/index.ui", f"/{MOD}/provider_roi.ui", + f"/{MOD}/menu.ui", f"/{MOD}/stat_avg_ttft.ui", f"/{MOD}/stat_success_rate.ui", f"/{MOD}/stat_total_calls.ui", f"/{MOD}/stat_fail_calls.ui", + f"/{MOD}/stat_today_usage.ui", + f"/{MOD}/stat_today_amount.ui", f"/{MOD}/api/%", ] diff --git a/wwwroot/api/customer_models.dspy b/wwwroot/api/customer_models.dspy new file mode 100644 index 0000000..1a29e7d --- /dev/null +++ b/wwwroot/api/customer_models.dspy @@ -0,0 +1,4 @@ +# 客户用量 API +import json +data = await api_customer_models(request) +return json.dumps(data, ensure_ascii=False) diff --git a/wwwroot/api/daily_trend.dspy b/wwwroot/api/daily_trend.dspy new file mode 100644 index 0000000..0580602 --- /dev/null +++ b/wwwroot/api/daily_trend.dspy @@ -0,0 +1,4 @@ +# 7天趋势 API +import json +data = await api_daily_trend(request) +return json.dumps(data, ensure_ascii=False) diff --git a/wwwroot/api/hourly_concurrency.dspy b/wwwroot/api/hourly_concurrency.dspy new file mode 100644 index 0000000..be2fe7d --- /dev/null +++ b/wwwroot/api/hourly_concurrency.dspy @@ -0,0 +1,4 @@ +# 每小时并发 API +import json +data = await api_hourly_concurrency(request) +return json.dumps(data, ensure_ascii=False) diff --git a/wwwroot/api/stats.dspy b/wwwroot/api/stats.dspy new file mode 100644 index 0000000..3096c04 --- /dev/null +++ b/wwwroot/api/stats.dspy @@ -0,0 +1,4 @@ +# 统计卡片 JSON API +import json +data = await api_stats(request) +return json.dumps(data, ensure_ascii=False) diff --git a/wwwroot/api/top_models.dspy b/wwwroot/api/top_models.dspy new file mode 100644 index 0000000..bc6bf0d --- /dev/null +++ b/wwwroot/api/top_models.dspy @@ -0,0 +1,4 @@ +# Top 模型 API +import json +data = await api_top_models(request) +return json.dumps(data, ensure_ascii=False) diff --git a/wwwroot/api/top_providers.dspy b/wwwroot/api/top_providers.dspy new file mode 100644 index 0000000..588fb5c --- /dev/null +++ b/wwwroot/api/top_providers.dspy @@ -0,0 +1,4 @@ +# Top 供应商 API +import json +data = await api_top_providers(request) +return json.dumps(data, ensure_ascii=False) diff --git a/wwwroot/api/top_users.dspy b/wwwroot/api/top_users.dspy new file mode 100644 index 0000000..9898f83 --- /dev/null +++ b/wwwroot/api/top_users.dspy @@ -0,0 +1,4 @@ +# Top 用户 API +import json +data = await api_top_users(request) +return json.dumps(data, ensure_ascii=False) diff --git a/wwwroot/index.ui b/wwwroot/index.ui index 0b62b6a..642548e 100644 --- a/wwwroot/index.ui +++ b/wwwroot/index.ui @@ -1,3 +1,4 @@ +{% set roles = get_user_roles(get_user()) %} { "widgettype": "VBox", "id": "datamart_root", @@ -11,46 +12,157 @@ "widgettype": "HBox", "options": {"width": "100%", "alignItems": "center", "marginBottom": "12px"}, "subwidgets": [ - {"widgettype": "Title2", "options": {"fontWeight": "700", "otext": "模型性能监控", "i18n": true}}, - {"widgettype": "Filler"}, - { - "widgettype": "Button", - "options": {"label": "刷新", "fontSize": "12px"}, - "binds": [{"wid": "self", "event": "click", "actiontype": "method", "target": "-@Cols", "method": "render_urldata", "params": {}}] - } + {"widgettype": "Title2", "options": {"fontWeight": "700", "otext": "数据概览", "i18n": true}}, + {"widgettype": "Filler"} ] }, { "widgettype": "HBox", "options": {"width": "100%", "gap": "16px", "marginBottom": "16px"}, "subwidgets": [ - {"widgettype": "RefreshWidget", "id": "stat_avg_ttft", "options": {"period_seconds": 60, "url": "{{entire_url('stat_avg_ttft.ui')}}", "width": "25%"}}, - {"widgettype": "RefreshWidget", "id": "stat_success_rate", "options": {"period_seconds": 60, "url": "{{entire_url('stat_success_rate.ui')}}", "width": "25%"}}, - {"widgettype": "RefreshWidget", "id": "stat_total_calls", "options": {"period_seconds": 60, "url": "{{entire_url('stat_total_calls.ui')}}", "width": "25%"}}, - {"widgettype": "RefreshWidget", "id": "stat_fail_calls", "options": {"period_seconds": 60, "url": "{{entire_url('stat_fail_calls.ui')}}", "width": "25%"}} + {"widgettype": "RefreshWidget", "id": "stat_today_usage", "options": {"period_seconds": 30, "url": "{{entire_url('stat_today_usage.ui')}}", "width": "16%"}}, + {"widgettype": "RefreshWidget", "id": "stat_today_amount", "options": {"period_seconds": 30, "url": "{{entire_url('stat_today_amount.ui')}}", "width": "16%"}}, + {"widgettype": "RefreshWidget", "id": "stat_success_rate", "options": {"period_seconds": 60, "url": "{{entire_url('stat_success_rate.ui')}}", "width": "16%"}}, + {"widgettype": "RefreshWidget", "id": "stat_fail_calls", "options": {"period_seconds": 60, "url": "{{entire_url('stat_fail_calls.ui')}}", "width": "16%"}}, + {"widgettype": "RefreshWidget", "id": "stat_avg_ttft", "options": {"period_seconds": 60, "url": "{{entire_url('stat_avg_ttft.ui')}}", "width": "16%"}}, + {"widgettype": "RefreshWidget", "id": "stat_total_users", "options": {"period_seconds": 60, "url": "{{entire_url('stat_total_users.ui')}}", "width": "16%"}} ] }, { - "widgettype": "Cols", - "options": { - "css": "card", - "col_cwidth": 28, - "title": "模型性能排行", - "data_url": "{{entire_url('api/model_perf.dspy')}}", - "data_params": {}, - "record_view": { - "widgettype": "HBox", - "options": {"cheight": 4, "width": "100%"}, + "widgettype": "HBox", + "options": {"width": "100%", "gap": "16px", "marginBottom": "16px"}, + "subwidgets": [ + { + "widgettype": "VBox", + "options": {"css": "card", "width": "50%", "padding": "16px"}, "subwidgets": [ - {"widgettype": "Text", "options": {"text": "${model}", "width": "30%"}}, - {"widgettype": "Text", "options": {"text": "${avg_ttft_ms}ms", "width": "15%"}}, - {"widgettype": "Text", "options": {"text": "${avg_ttot_ms}ms", "width": "15%"}}, - {"widgettype": "Text", "options": {"text": "${success_rate}%", "width": "15%"}}, - {"widgettype": "Text", "options": {"text": "${total_calls}", "width": "15%"}}, - {"widgettype": "Text", "options": {"text": "¥${total_amount}", "width": "10%"}} + {"widgettype": "Title4", "options": {"fontWeight": "600", "otext": "模型性能排行", "i18n": true, "marginBottom": "8px"}}, + { + "widgettype": "Cols", + "options": { + "col_cwidth": 24, + "data_url": "{{entire_url('api/model_perf.dspy')}}", + "data_params": {}, + "record_view": { + "widgettype": "HBox", + "options": {"cheight": 3, "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "${model}", "width": "35%"}}, + {"widgettype": "Text", "options": {"text": "${avg_ttft_ms}ms", "width": "15%"}}, + {"widgettype": "Text", "options": {"text": "${success_rate}%", "width": "15%"}}, + {"widgettype": "Text", "options": {"text": "${total_calls}", "width": "15%"}}, + {"widgettype": "Text", "options": {"text": "¥${total_amount}", "width": "20%"}} + ] + } + } + } + ] + }, + { + "widgettype": "VBox", + "options": {"css": "card", "width": "50%", "padding": "16px"}, + "subwidgets": [ + {"widgettype": "Title4", "options": {"fontWeight": "600", "otext": "供应商性价比", "i18n": true, "marginBottom": "8px"}}, + { + "widgettype": "Cols", + "options": { + "col_cwidth": 24, + "data_url": "{{entire_url('api/provider_roi.dspy')}}", + "data_params": {}, + "record_view": { + "widgettype": "HBox", + "options": {"cheight": 3, "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "${model}", "width": "35%"}}, + {"widgettype": "Text", "options": {"text": "¥${unit_price}/t", "width": "20%"}}, + {"widgettype": "Text", "options": {"text": "${avg_ttft_ms}ms", "width": "15%"}}, + {"widgettype": "Text", "options": {"text": "${total_calls}次", "width": "15%"}}, + {"widgettype": "Text", "options": {"text": "¥${total_amount}", "width": "15%"}} + ] + } + } + } ] } - } + ] + }, + { + "widgettype": "HBox", + "options": {"width": "100%", "gap": "16px"}, + "subwidgets": [ + { + "widgettype": "VBox", + "options": {"css": "card", "width": "33%", "padding": "16px"}, + "subwidgets": [ + {"widgettype": "Title4", "options": {"fontWeight": "600", "otext": "热门模型", "i18n": true, "marginBottom": "8px"}}, + { + "widgettype": "Cols", + "options": { + "col_cwidth": 20, + "data_url": "{{entire_url('api/top_models.dspy')}}", + "data_params": {}, + "record_view": { + "widgettype": "HBox", + "options": {"cheight": 3, "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "${model_name}", "width": "50%"}}, + {"widgettype": "Text", "options": {"text": "${cnt}次", "width": "25%"}}, + {"widgettype": "Text", "options": {"text": "¥${total_amount}", "width": "25%"}} + ] + } + } + } + ] + }, + { + "widgettype": "VBox", + "options": {"css": "card", "width": "33%", "padding": "16px"}, + "subwidgets": [ + {"widgettype": "Title4", "options": {"fontWeight": "600", "otext": "用户排行", "i18n": true, "marginBottom": "8px"}}, + { + "widgettype": "Cols", + "options": { + "col_cwidth": 20, + "data_url": "{{entire_url('api/top_users.dspy')}}", + "data_params": {}, + "record_view": { + "widgettype": "HBox", + "options": {"cheight": 3, "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "${user_name}", "width": "50%"}}, + {"widgettype": "Text", "options": {"text": "${cnt}次", "width": "25%"}}, + {"widgettype": "Text", "options": {"text": "¥${total_amount}", "width": "25%"}} + ] + } + } + } + ] + }, + { + "widgettype": "VBox", + "options": {"css": "card", "width": "33%", "padding": "16px"}, + "subwidgets": [ + {"widgettype": "Title4", "options": {"fontWeight": "600", "otext": "供应商排行", "i18n": true, "marginBottom": "8px"}}, + { + "widgettype": "Cols", + "options": { + "col_cwidth": 20, + "data_url": "{{entire_url('api/top_providers.dspy')}}", + "data_params": {}, + "record_view": { + "widgettype": "HBox", + "options": {"cheight": 3, "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "${provider_name}", "width": "50%"}}, + {"widgettype": "Text", "options": {"text": "${cnt}次", "width": "25%"}}, + {"widgettype": "Text", "options": {"text": "¥${total_amount}", "width": "25%"}} + ] + } + } + } + ] + } + ] } ] } diff --git a/wwwroot/stat_success_rate.ui b/wwwroot/stat_success_rate.ui index daaad73..f4e5856 100644 --- a/wwwroot/stat_success_rate.ui +++ b/wwwroot/stat_success_rate.ui @@ -1,12 +1,5 @@ -{% set rows = api_model_perf(request) %} +{% set rows = api_model_perf(None) %} {% set total = 0 %}{% set succ = 0 %} {% for r in rows %}{% set total = total + r.total_calls %}{% set succ = succ + r.success_calls %}{% endfor %} {% set rate = (succ * 100 / total)|round(1) if total > 0 else 0 %} -{ - "widgettype": "VBox", - "options": {"css": "card", "padding": "16px", "borderRadius": "12px", "width": "100%"}, - "subwidgets": [ - {"widgettype": "Text", "options": {"text": "成功率", "fontSize": "12px", "color": "var(--sage-text-secondary)"}}, - {"widgettype": "Title2", "options": {"text": "{{rate}}%", "fontWeight": "700", "marginTop": "4px"}} - ] -} +{"widgettype":"VBox","options":{"css":"card","padding":"16px","borderRadius":"12px","width":"100%"},"subwidgets":[{"widgettype":"Text","options":{"text":"成功率","fontSize":"12px","color":"var(--sage-text-secondary)"}},{"widgettype":"Title2","options":{"text":"{{rate}}%","fontWeight":"700","marginTop":"4px"}}]} diff --git a/wwwroot/stat_today_amount.ui b/wwwroot/stat_today_amount.ui new file mode 100644 index 0000000..a4c1242 --- /dev/null +++ b/wwwroot/stat_today_amount.ui @@ -0,0 +1,9 @@ +{% set amt = j2_today_amount(request) %} +{ + "widgettype": "VBox", + "options": {"css": "card", "padding": "16px", "borderRadius": "12px", "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "今日金额", "fontSize": "12px", "color": "var(--sage-text-secondary)"}}, + {"widgettype": "Title2", "options": {"text": "¥{{'%.2f' % amt}}", "fontWeight": "700", "marginTop": "4px"}} + ] +} diff --git a/wwwroot/stat_today_usage.ui b/wwwroot/stat_today_usage.ui new file mode 100644 index 0000000..42d65aa --- /dev/null +++ b/wwwroot/stat_today_usage.ui @@ -0,0 +1,9 @@ +{% set cnt = j2_today_usage(request) %} +{ + "widgettype": "VBox", + "options": {"css": "card", "padding": "16px", "borderRadius": "12px", "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "今日调用", "fontSize": "12px", "color": "var(--sage-text-secondary)"}}, + {"widgettype": "Title2", "options": {"text": "{{cnt}}", "fontWeight": "700", "marginTop": "4px"}} + ] +}