dashboard_for_sage/wwwroot/api/dashboard_cards.dspy
yumoqing d2210a2996 refactor: use RefreshWidget for stat cards + fix .dspy import violations
Architecture:
- index.ui: title + RefreshWidget(cards) + ChartBar with refresh_period
- RefreshWidget wraps dashboard_cards.dspy → returns full card widget tree
  with live data (cnt, amount, total_users, concurrent_users)
- ChartBar handles its own auto-refresh via refresh_period: 10
- No more JS polling file needed

.dspy import fixes:
- get_today_usage.dspy: remove import json, from datetime import date
- get_user_stats.dspy: remove from datetime import datetime, timedelta
- get_top_models.dspy: remove from datetime import date
- All use pre-loaded datetime module (datetime.date.today(), etc.)
- dashboard_cards.dspy: same pattern, no imports

Permission:
- load_path.py: add dashboard_cards.dspy logined
2026-05-24 16:45:45 +08:00

74 lines
3.8 KiB
Plaintext

"""获取当天llmusage笔数和交易金额 + 用户统计 — 返回完整卡片widget树供RefreshWidget加载"""
# datetime, json, DBPools 由 ahserver 预加载,无需 import
today = datetime.date.today().isoformat()
now = datetime.datetime.now()
five_min_ago = (now - datetime.timedelta(minutes=5)).strftime('%Y-%m-%d %H:%M:%S')
db = DBPools()
# 今日用量
async with db.sqlorContext('sage') as sor:
recs = await sor.sqlExe(
"SELECT COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount FROM llmusage WHERE use_date = ${today}$",
{'today': today}
)
cnt = int(recs[0].get('cnt', 0)) if recs else 0
total_amount = float(recs[0].get('total_amount', 0)) if recs else 0.0
# 总用户数
async with db.sqlorContext('sage') as sor:
user_recs = await sor.sqlExe("SELECT COUNT(*) as total_users FROM users", {})
total_users = int(user_recs[0].get('total_users', 0)) if user_recs else 0
# 并发用户
async with db.sqlorContext('sage') as sor:
conc = await sor.sqlExe(
"SELECT COUNT(DISTINCT userid) as concurrent_users FROM llmusage WHERE use_date = ${today}$ AND use_time >= ${five_min_ago}$",
{'today': today, 'five_min_ago': five_min_ago}
)
concurrent_users = int(conc[0].get('concurrent_users', 0)) if conc else 0
return {
"widgettype": "ResponsableBox",
"options": {"gap": "16px", "minWidth": "250px"},
"subwidgets": [
{
"widgettype": "VBox",
"id": "card_today_cnt",
"options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "今日调用笔数", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}},
{"widgettype": "Text", "id": "today_cnt_value", "options": {"text": str(cnt), "fontSize": "32px", "fontWeight": "bold", "color": "#1890ff"}}
]
},
{
"widgettype": "VBox",
"id": "card_today_amount",
"options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "今日交易金额", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}},
{"widgettype": "Text", "id": "today_amount_value", "options": {"text": "\u00a5%.2f" % total_amount, "fontSize": "32px", "fontWeight": "bold", "color": "#52c41a"}}
]
},
{
"widgettype": "VBox",
"id": "card_total_users",
"options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "用户总数", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}},
{"widgettype": "Text", "id": "total_users_value", "options": {"text": str(total_users), "fontSize": "32px", "fontWeight": "bold", "color": "#722ed1"}}
]
},
{
"widgettype": "VBox",
"id": "card_concurrent_users",
"options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "当前并发用户", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}},
{"widgettype": "Text", "id": "concurrent_users_value", "options": {"text": str(concurrent_users), "fontSize": "32px", "fontWeight": "bold", "color": "#fa8c16"}}
]
}
]
}