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 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")

View File

@ -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/%",
]

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",
"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%"}}
]
}
}
}
]
}
]
}
]
}

View File

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

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