From d2210a29960ba0811b4d9f37a2c1fc55934ee465 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sun, 24 May 2026 16:45:45 +0800 Subject: [PATCH] refactor: use RefreshWidget for stat cards + fix .dspy import violations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- wwwroot/api/dashboard_cards.dspy | 73 +++++++++++++++++++++ wwwroot/api/dashboard_content.dspy | 100 ----------------------------- wwwroot/api/get_today_usage.dspy | 6 +- wwwroot/api/get_top_models.dspy | 5 +- wwwroot/api/get_user_stats.dspy | 16 ++--- wwwroot/index.ui | 40 +++++++++++- 6 files changed, 121 insertions(+), 119 deletions(-) create mode 100644 wwwroot/api/dashboard_cards.dspy delete mode 100644 wwwroot/api/dashboard_content.dspy diff --git a/wwwroot/api/dashboard_cards.dspy b/wwwroot/api/dashboard_cards.dspy new file mode 100644 index 0000000..83a57f2 --- /dev/null +++ b/wwwroot/api/dashboard_cards.dspy @@ -0,0 +1,73 @@ +"""获取当天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"}} + ] + } + ] +} diff --git a/wwwroot/api/dashboard_content.dspy b/wwwroot/api/dashboard_content.dspy deleted file mode 100644 index 0aa5375..0000000 --- a/wwwroot/api/dashboard_content.dspy +++ /dev/null @@ -1,100 +0,0 @@ -"""返回 dashboard 动态 UI 描述(含实时数据)""" -from datetime import date, datetime, timedelta - -db = DBPools() -today = date.today().isoformat() -now = datetime.now() -five_min_ago = (now - timedelta(minutes=5)).strftime('%Y-%m-%d %H:%M:%S') - -# 今日用量 -async with db.sqlorContext('sage') as sor: - usage = await sor.sqlExe( - "SELECT COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount FROM llmusage WHERE use_date = ${today}$", - {'today': today} - ) - cnt = int(usage[0]['cnt']) if usage else 0 - total_amount = float(usage[0]['total_amount']) if usage else 0.0 - -# 总用户数 -async with db.sqlorContext('sage') as sor: - users = await sor.sqlExe("SELECT COUNT(*) as total_users FROM users", {}) - total_users = int(users[0]['total_users']) if users 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]['concurrent_users']) if conc else 0 - -# ChartBar data_url — 用相对路径,bricks 会基于当前上下文解析 -chart_data_url = 'api/get_top_models.dspy' - -return { - "widgettype": "VBox", - "options": {"width": "100%"}, - "subwidgets": [ - { - "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": f"\u00a5{total_amount:.2f}", "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"}} - ] - } - ] - }, - { - "widgettype": "VBox", - "id": "chart_section", - "options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "marginTop": "20px", "minHeight": "350px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"}, - "subwidgets": [ - {"widgettype": "Text", "options": {"text": "Top 3 模型(今日)", "fontSize": "18px", "fontWeight": "bold", "color": "#333", "marginBottom": "16px"}}, - { - "widgettype": "ChartBar", - "id": "top_models_chart", - "options": { - "height": "300px", - "width": "100%", - "data_url": chart_data_url, - "nameField": "model_name", - "valueFields": ["cnt", "total_amount"] - } - } - ] - } - ] -} diff --git a/wwwroot/api/get_today_usage.dspy b/wwwroot/api/get_today_usage.dspy index d54a3e1..3e3c3e4 100644 --- a/wwwroot/api/get_today_usage.dspy +++ b/wwwroot/api/get_today_usage.dspy @@ -1,8 +1,7 @@ """获取当天llmusage笔数和交易金额""" -import json -from datetime import date +# datetime, json, DBPools 由 ahserver 预加载,无需 import -ns = params_kw.copy() +today = datetime.date.today().isoformat() sql = """ SELECT @@ -12,7 +11,6 @@ FROM llmusage WHERE use_date = ${today}$ """ -today = date.today().isoformat() db = DBPools() async with db.sqlorContext('sage') as sor: recs = await sor.sqlExe(sql, {'today': today}) diff --git a/wwwroot/api/get_top_models.dspy b/wwwroot/api/get_top_models.dspy index 06d502d..8d63e2c 100644 --- a/wwwroot/api/get_top_models.dspy +++ b/wwwroot/api/get_top_models.dspy @@ -1,8 +1,7 @@ """获取当天排名前三的模型数量和金额""" -from datetime import date +# datetime, json, DBPools 由 ahserver 预加载,无需 import -ns = params_kw.copy() -today = date.today().isoformat() +today = datetime.date.today().isoformat() sql = """ SELECT diff --git a/wwwroot/api/get_user_stats.dspy b/wwwroot/api/get_user_stats.dspy index fd5210e..67903c8 100644 --- a/wwwroot/api/get_user_stats.dspy +++ b/wwwroot/api/get_user_stats.dspy @@ -1,17 +1,12 @@ """获取当前用户总数和并发用户数(近5分钟有活跃记录的用户)""" -from datetime import datetime, timedelta +# datetime, json, DBPools 由 ahserver 预加载,无需 import -ns = params_kw.copy() -db = DBPools() - -# 总用户数 -sql_users = "SELECT COUNT(*) as total_users FROM users" - -# 并发用户数:近5分钟内在llmusage中有记录的不同用户 -now = datetime.now() -five_min_ago = (now - timedelta(minutes=5)).strftime('%Y-%m-%d %H:%M:%S') +now = datetime.datetime.now() +five_min_ago = (now - datetime.timedelta(minutes=5)).strftime('%Y-%m-%d %H:%M:%S') today = now.strftime('%Y-%m-%d') +sql_users = "SELECT COUNT(*) as total_users FROM users" + sql_concurrent = """ SELECT COUNT(DISTINCT userid) as concurrent_users FROM llmusage @@ -19,6 +14,7 @@ WHERE use_date = ${today}$ AND use_time >= ${five_min_ago}$ """ +db = DBPools() async with db.sqlorContext('sage') as sor: user_recs = await sor.sqlExe(sql_users, {}) total_users = int(user_recs[0].get('total_users', 0)) if user_recs else 0 diff --git a/wwwroot/index.ui b/wwwroot/index.ui index 23c1af9..ab0497c 100644 --- a/wwwroot/index.ui +++ b/wwwroot/index.ui @@ -19,11 +19,47 @@ }, { "widgettype": "RefreshWidget", - "id": "dashboard_refresh", + "id": "cards_refresh", "options": { "period_seconds": 10, - "url": "{{entire_url('api/dashboard_content.dspy')}}" + "url": "{{entire_url('api/dashboard_cards.dspy')}}" } + }, + { + "widgettype": "VBox", + "id": "chart_section", + "options": { + "bgcolor": "#FFFFFF", + "padding": "24px", + "borderRadius": "8px", + "marginTop": "20px", + "minHeight": "350px", + "boxShadow": "0 2px 8px rgba(0,0,0,0.1)" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "text": "Top 3 模型(今日)", + "fontSize": "18px", + "fontWeight": "bold", + "color": "#333", + "marginBottom": "16px" + } + }, + { + "widgettype": "ChartBar", + "id": "top_models_chart", + "options": { + "height": "300px", + "width": "100%", + "data_url": "{{entire_url('api/get_top_models.dspy')}}", + "nameField": "model_name", + "valueFields": ["cnt", "total_amount"], + "refresh_period": 10 + } + } + ] } ] }