feat: unified dashboard — all dashboard_for_sage metrics from data mart, 6 stat cards, perf+ops

This commit is contained in:
yumoqing 2026-07-17 18:11:19 +08:00
parent 92572bcf6b
commit 054b0dd8e2
14 changed files with 417 additions and 66 deletions

136
sage_datamark/dashboards.py Normal file
View File

@ -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]

View File

@ -1,23 +1,24 @@
"""sage_datamark 模块初始化""" """sage_datamark 模块初始化 — 统一 Dashboard API"""
from ahserver.serverenv import ServerEnv from ahserver.serverenv import ServerEnv
from appPublic.jsonConfig import getConfig from appPublic.jsonConfig import getConfig
from appPublic.log import debug, exception 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 .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" MODULE_NAME = "sage_datamark"
def get_dbname(): # ── 性能指标 API ──
env = ServerEnv()
return env.get_module_dbname(MODULE_NAME)
async def api_model_perf(sor, params_kw=None): 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 model = params_kw.get('model') if params_kw else None
stat_date = params_kw.get('date') 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" sql = "SELECT * FROM dm_model_perf_daily WHERE 1=1"
ns = {} ns = {}
if stat_date: if stat_date:
@ -27,16 +28,13 @@ async def api_model_perf(sor, params_kw=None):
sql += " AND model=${model}$" sql += " AND model=${model}$"
ns['model'] = model ns['model'] = model
sql += " ORDER BY total_calls DESC LIMIT 50" sql += " ORDER BY total_calls DESC LIMIT 50"
rows = await sor.sqlExe(sql, ns) rows = await sor.sqlExe(sql, ns)
return [dict(r) for r in rows] return [dict(r) for r in rows]
async def api_provider_roi(sor, params_kw=None): 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 model = params_kw.get('model') if params_kw else None
stat_date = params_kw.get('date') 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" sql = "SELECT * FROM dm_provider_cost_daily WHERE 1=1"
ns = {} ns = {}
if stat_date: if stat_date:
@ -46,33 +44,96 @@ async def api_provider_roi(sor, params_kw=None):
sql += " AND model=${model}$" sql += " AND model=${model}$"
ns['model'] = model ns['model'] = model
sql += " ORDER BY total_amount DESC LIMIT 50" sql += " ORDER BY total_amount DESC LIMIT 50"
rows = await sor.sqlExe(sql, ns) rows = await sor.sqlExe(sql, ns)
return [dict(r) for r in rows] return [dict(r) for r in rows]
async def api_call_detail(sor, params_kw=None): # ── 运营指标 API替代 dashboard_for_sage──
"""调用明细 API — POST /sage_datamark/api/call_detail.dspy"""
stat_date = (params_kw or {}).get('date')
userorgid = (params_kw or {}).get('userorgid')
sql = "SELECT * FROM dm_model_call_fact WHERE 1=1" async def api_stats(request):
ns = {} """统计卡片数据 — /sage_datamark/api/stats.dspy"""
if stat_date: env = request._run_ns
sql += " AND call_date=${stat_date}$" async with get_sor_context(env, MODULE_NAME) as sor:
ns['stat_date'] = stat_date return {
if userorgid: 'today_usage': await get_today_usage(env, sor),
sql += " AND userorgid=${userorgid}$" 'today_amount': await get_today_amount(env, sor),
ns['userorgid'] = userorgid 'success_rate': await get_success_rate(env, sor),
sql += " ORDER BY call_time DESC LIMIT 200" '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(): def load_sage_datamark():
env = ServerEnv() env = ServerEnv()
env.api_model_perf = api_model_perf env.api_model_perf = api_model_perf
env.api_provider_roi = api_provider_roi env.api_provider_roi = api_provider_roi
env.api_call_detail = api_call_detail env.api_stats = api_stats
debug("[sage_datamark] registered API endpoints") 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")

View File

@ -11,10 +11,13 @@ PATHS_LOGINED = [
f"/{MOD}", f"/{MOD}",
f"/{MOD}/index.ui", f"/{MOD}/index.ui",
f"/{MOD}/provider_roi.ui", f"/{MOD}/provider_roi.ui",
f"/{MOD}/menu.ui",
f"/{MOD}/stat_avg_ttft.ui", f"/{MOD}/stat_avg_ttft.ui",
f"/{MOD}/stat_success_rate.ui", f"/{MOD}/stat_success_rate.ui",
f"/{MOD}/stat_total_calls.ui", f"/{MOD}/stat_total_calls.ui",
f"/{MOD}/stat_fail_calls.ui", f"/{MOD}/stat_fail_calls.ui",
f"/{MOD}/stat_today_usage.ui",
f"/{MOD}/stat_today_amount.ui",
f"/{MOD}/api/%", f"/{MOD}/api/%",
] ]

View File

@ -0,0 +1,4 @@
# 客户用量 API
import json
data = await api_customer_models(request)
return json.dumps(data, ensure_ascii=False)

View File

@ -0,0 +1,4 @@
# 7天趋势 API
import json
data = await api_daily_trend(request)
return json.dumps(data, ensure_ascii=False)

View File

@ -0,0 +1,4 @@
# 每小时并发 API
import json
data = await api_hourly_concurrency(request)
return json.dumps(data, ensure_ascii=False)

4
wwwroot/api/stats.dspy Normal file
View File

@ -0,0 +1,4 @@
# 统计卡片 JSON API
import json
data = await api_stats(request)
return json.dumps(data, ensure_ascii=False)

View File

@ -0,0 +1,4 @@
# Top 模型 API
import json
data = await api_top_models(request)
return json.dumps(data, ensure_ascii=False)

View File

@ -0,0 +1,4 @@
# Top 供应商 API
import json
data = await api_top_providers(request)
return json.dumps(data, ensure_ascii=False)

View File

@ -0,0 +1,4 @@
# Top 用户 API
import json
data = await api_top_users(request)
return json.dumps(data, ensure_ascii=False)

View File

@ -1,3 +1,4 @@
{% set roles = get_user_roles(get_user()) %}
{ {
"widgettype": "VBox", "widgettype": "VBox",
"id": "datamart_root", "id": "datamart_root",
@ -11,48 +12,159 @@
"widgettype": "HBox", "widgettype": "HBox",
"options": {"width": "100%", "alignItems": "center", "marginBottom": "12px"}, "options": {"width": "100%", "alignItems": "center", "marginBottom": "12px"},
"subwidgets": [ "subwidgets": [
{"widgettype": "Title2", "options": {"fontWeight": "700", "otext": "模型性能监控", "i18n": true}}, {"widgettype": "Title2", "options": {"fontWeight": "700", "otext": "数据概览", "i18n": true}},
{"widgettype": "Filler"}, {"widgettype": "Filler"}
{
"widgettype": "Button",
"options": {"label": "刷新", "fontSize": "12px"},
"binds": [{"wid": "self", "event": "click", "actiontype": "method", "target": "-@Cols", "method": "render_urldata", "params": {}}]
}
] ]
}, },
{ {
"widgettype": "HBox", "widgettype": "HBox",
"options": {"width": "100%", "gap": "16px", "marginBottom": "16px"}, "options": {"width": "100%", "gap": "16px", "marginBottom": "16px"},
"subwidgets": [ "subwidgets": [
{"widgettype": "RefreshWidget", "id": "stat_avg_ttft", "options": {"period_seconds": 60, "url": "{{entire_url('stat_avg_ttft.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_success_rate", "options": {"period_seconds": 60, "url": "{{entire_url('stat_success_rate.ui')}}", "width": "25%"}}, {"widgettype": "RefreshWidget", "id": "stat_today_amount", "options": {"period_seconds": 30, "url": "{{entire_url('stat_today_amount.ui')}}", "width": "16%"}},
{"widgettype": "RefreshWidget", "id": "stat_total_calls", "options": {"period_seconds": 60, "url": "{{entire_url('stat_total_calls.ui')}}", "width": "25%"}}, {"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": "25%"}} {"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": "HBox",
"options": {"width": "100%", "gap": "16px", "marginBottom": "16px"},
"subwidgets": [
{
"widgettype": "VBox",
"options": {"css": "card", "width": "50%", "padding": "16px"},
"subwidgets": [
{"widgettype": "Title4", "options": {"fontWeight": "600", "otext": "模型性能排行", "i18n": true, "marginBottom": "8px"}},
{ {
"widgettype": "Cols", "widgettype": "Cols",
"options": { "options": {
"css": "card", "col_cwidth": 24,
"col_cwidth": 28,
"title": "模型性能排行",
"data_url": "{{entire_url('api/model_perf.dspy')}}", "data_url": "{{entire_url('api/model_perf.dspy')}}",
"data_params": {}, "data_params": {},
"record_view": { "record_view": {
"widgettype": "HBox", "widgettype": "HBox",
"options": {"cheight": 4, "width": "100%"}, "options": {"cheight": 3, "width": "100%"},
"subwidgets": [ "subwidgets": [
{"widgettype": "Text", "options": {"text": "${model}", "width": "30%"}}, {"widgettype": "Text", "options": {"text": "${model}", "width": "35%"}},
{"widgettype": "Text", "options": {"text": "${avg_ttft_ms}ms", "width": "15%"}}, {"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": "${success_rate}%", "width": "15%"}},
{"widgettype": "Text", "options": {"text": "${total_calls}", "width": "15%"}}, {"widgettype": "Text", "options": {"text": "${total_calls}", "width": "15%"}},
{"widgettype": "Text", "options": {"text": "¥${total_amount}", "width": "10%"}} {"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%"}}
]
}
}
}
]
}
]
}
]
} }
] ]
} }

View File

@ -1,12 +1,5 @@
{% set rows = api_model_perf(request) %} {% set rows = api_model_perf(None) %}
{% set total = 0 %}{% set succ = 0 %} {% set total = 0 %}{% set succ = 0 %}
{% for r in rows %}{% set total = total + r.total_calls %}{% set succ = succ + r.success_calls %}{% endfor %} {% 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 %} {% 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"}}
]
}

View File

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

View File

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