diff --git a/dashboard_for_sage/init.py b/dashboard_for_sage/init.py index 96ab9da..3436333 100644 --- a/dashboard_for_sage/init.py +++ b/dashboard_for_sage/init.py @@ -1,12 +1 @@ -from ahserver.serverenv import ServerEnv - -MODULE_NAME = "dashboard_for_sage" -MODULE_VERSION = "1.0.0" - - -def load_dashboard_for_sage(): - """Register dashboard module with ServerEnv.""" - env = ServerEnv() - # Dashboard is pure display - all logic is in .dspy files - # which use globals() functions directly. - return True +from .load_dashboard import load_dashboard diff --git a/dashboard_for_sage/load_dashboard.py b/dashboard_for_sage/load_dashboard.py new file mode 100644 index 0000000..e95f302 --- /dev/null +++ b/dashboard_for_sage/load_dashboard.py @@ -0,0 +1,92 @@ +"""Dashboard data functions - exposed via load_dashboard() on ServerEnv""" +from ahserver.serverenv import ServerEnv +from sqlor.dbpools import get_sor_context +from datetime import datetime, timedelta, date + + +async def get_today_usage(request): + """获取当天llmusage笔数""" + today = date.today().isoformat() + env = request._run_ns + async with get_sor_context(env, 'sage') as sor: + sql = "SELECT COUNT(*) as cnt FROM llmusage WHERE use_date = ${today}$" + recs = await sor.sqlExe(sql, {'today': today}) + cnt = int(recs[0].get('cnt', 0)) if recs else 0 + return cnt + + +async def get_today_amount(request): + """获取当天交易金额""" + today = date.today().isoformat() + env = request._run_ns + async with get_sor_context(env, 'sage') as sor: + sql = "SELECT COALESCE(SUM(amount), 0) as total_amount FROM llmusage WHERE use_date = ${today}$" + recs = await sor.sqlExe(sql, {'today': today}) + amount = float(recs[0].get('total_amount', 0)) if recs else 0.0 + return amount + + +async def get_total_users(request): + """获取用户总数""" + env = request._run_ns + async with get_sor_context(env, 'sage') as sor: + sql = "SELECT COUNT(*) as total_users FROM users" + recs = await sor.sqlExe(sql, {}) + total = int(recs[0].get('total_users', 0)) if recs else 0 + return total + + +async def get_concurrent_users(request): + """获取当前并发用户数(近5分钟有活跃记录的用户)""" + now = datetime.now() + five_min_ago = (now - timedelta(minutes=5)).strftime('%Y-%m-%d %H:%M:%S') + today = now.strftime('%Y-%m-%d') + env = request._run_ns + async with get_sor_context(env, 'sage') as sor: + sql = """ + SELECT COUNT(DISTINCT userid) as concurrent_users + FROM llmusage + WHERE use_date = ${today}$ + AND use_time >= ${five_min_ago}$ + """ + recs = await sor.sqlExe(sql, {'today': today, 'five_min_ago': five_min_ago}) + cnt = int(recs[0].get('concurrent_users', 0)) if recs else 0 + return cnt + + +async def get_top_models(request): + """获取当天排名前三的模型(返回记录列表,供ChartBar使用)""" + today = date.today().isoformat() + env = request._run_ns + async with get_sor_context(env, 'sage') as sor: + sql = """ + SELECT + b.name as model_name, + COUNT(*) as cnt, + COALESCE(SUM(a.amount), 0) as total_amount + FROM llmusage a + LEFT JOIN llm b ON a.llmid = b.id + WHERE a.use_date = ${today}$ + GROUP BY a.llmid, b.name + ORDER BY cnt DESC + LIMIT 3 + """ + recs = await sor.sqlExe(sql, {'today': today}) + result = [] + for r in recs: + result.append({ + 'model_name': r.get('model_name', 'Unknown'), + 'cnt': int(r.get('cnt', 0)), + 'total_amount': float(r.get('total_amount', 0)) + }) + return result + + +def load_dashboard(): + """Register dashboard functions on ServerEnv""" + g = ServerEnv() + g.get_today_usage = get_today_usage + g.get_today_amount = get_today_amount + g.get_total_users = get_total_users + g.get_concurrent_users = get_concurrent_users + g.get_top_models = get_top_models diff --git a/wwwroot/api/dashboard_cards.dspy b/wwwroot/api/dashboard_cards.dspy deleted file mode 100644 index 83a57f2..0000000 --- a/wwwroot/api/dashboard_cards.dspy +++ /dev/null @@ -1,73 +0,0 @@ -"""获取当天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/get_today_usage.dspy b/wwwroot/api/get_today_usage.dspy deleted file mode 100644 index 3e3c3e4..0000000 --- a/wwwroot/api/get_today_usage.dspy +++ /dev/null @@ -1,22 +0,0 @@ -"""获取当天llmusage笔数和交易金额""" -# datetime, json, DBPools 由 ahserver 预加载,无需 import - -today = datetime.date.today().isoformat() - -sql = """ -SELECT - COUNT(*) as cnt, - COALESCE(SUM(amount), 0) as total_amount -FROM llmusage -WHERE use_date = ${today}$ -""" - -db = DBPools() -async with db.sqlorContext('sage') as sor: - recs = await sor.sqlExe(sql, {'today': today}) - if recs: - return { - 'cnt': int(recs[0].get('cnt', 0)), - 'total_amount': float(recs[0].get('total_amount', 0)) - } -return {'cnt': 0, 'total_amount': 0} diff --git a/wwwroot/api/get_top_models.dspy b/wwwroot/api/get_top_models.dspy deleted file mode 100644 index 8d63e2c..0000000 --- a/wwwroot/api/get_top_models.dspy +++ /dev/null @@ -1,31 +0,0 @@ -"""获取当天排名前三的模型数量和金额""" -# datetime, json, DBPools 由 ahserver 预加载,无需 import - -today = datetime.date.today().isoformat() - -sql = """ -SELECT - b.name as model_name, - COUNT(*) as cnt, - COALESCE(SUM(a.amount), 0) as total_amount -FROM llmusage a -LEFT JOIN llm b ON a.llmid = b.id -WHERE a.use_date = ${today}$ -GROUP BY a.llmid, b.name -ORDER BY cnt DESC -LIMIT 3 -""" - -db = DBPools() -async with db.sqlorContext('sage') as sor: - recs = await sor.sqlExe(sql, {'today': today}) - result = [] - for r in recs: - result.append({ - 'model_name': r.get('model_name', 'Unknown'), - 'cnt': int(r.get('cnt', 0)), - 'total_amount': float(r.get('total_amount', 0)) - }) - return result - -return [] diff --git a/wwwroot/api/get_user_stats.dspy b/wwwroot/api/get_user_stats.dspy deleted file mode 100644 index 67903c8..0000000 --- a/wwwroot/api/get_user_stats.dspy +++ /dev/null @@ -1,31 +0,0 @@ -"""获取当前用户总数和并发用户数(近5分钟有活跃记录的用户)""" -# datetime, json, DBPools 由 ahserver 预加载,无需 import - -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 -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 - - conc_recs = await sor.sqlExe(sql_concurrent, { - 'today': today, - 'five_min_ago': five_min_ago - }) - concurrent_users = int(conc_recs[0].get('concurrent_users', 0)) if conc_recs else 0 - -return { - 'total_users': total_users, - 'concurrent_users': concurrent_users -} diff --git a/wwwroot/concurrent_users.ui b/wwwroot/concurrent_users.ui new file mode 100644 index 0000000..34ffca2 --- /dev/null +++ b/wwwroot/concurrent_users.ui @@ -0,0 +1,32 @@ +{ + "widgettype": "VBox", + "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": "{{get_concurrent_users(request)}}", + "fontSize": "32px", + "fontWeight": "bold", + "color": "#fa8c16" + } + } + ] +} diff --git a/wwwroot/index.ui b/wwwroot/index.ui index ab0497c..d1c7093 100644 --- a/wwwroot/index.ui +++ b/wwwroot/index.ui @@ -18,16 +18,48 @@ } }, { - "widgettype": "RefreshWidget", - "id": "cards_refresh", + "widgettype": "ResponsableBox", "options": { - "period_seconds": 10, - "url": "{{entire_url('api/dashboard_cards.dspy')}}" - } + "gap": "16px", + "minWidth": "250px" + }, + "subwidgets": [ + { + "widgettype": "RefreshWidget", + "id": "refresh_today_usage", + "options": { + "period_seconds": 10, + "url": "{{entire_url('today_usage.ui')}}" + } + }, + { + "widgettype": "RefreshWidget", + "id": "refresh_today_amount", + "options": { + "period_seconds": 10, + "url": "{{entire_url('today_amount.ui')}}" + } + }, + { + "widgettype": "RefreshWidget", + "id": "refresh_total_users", + "options": { + "period_seconds": 10, + "url": "{{entire_url('total_users.ui')}}" + } + }, + { + "widgettype": "RefreshWidget", + "id": "refresh_concurrent_users", + "options": { + "period_seconds": 10, + "url": "{{entire_url('concurrent_users.ui')}}" + } + } + ] }, { "widgettype": "VBox", - "id": "chart_section", "options": { "bgcolor": "#FFFFFF", "padding": "24px", @@ -48,15 +80,11 @@ } }, { - "widgettype": "ChartBar", - "id": "top_models_chart", + "widgettype": "RefreshWidget", + "id": "refresh_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 + "period_seconds": 10, + "url": "{{entire_url('top_models_chart.ui')}}" } } ] diff --git a/wwwroot/today_amount.ui b/wwwroot/today_amount.ui new file mode 100644 index 0000000..1354537 --- /dev/null +++ b/wwwroot/today_amount.ui @@ -0,0 +1,32 @@ +{ + "widgettype": "VBox", + "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": "¥{{get_today_amount(request)|round(2)}}", + "fontSize": "32px", + "fontWeight": "bold", + "color": "#52c41a" + } + } + ] +} diff --git a/wwwroot/today_usage.ui b/wwwroot/today_usage.ui new file mode 100644 index 0000000..fa2eaf5 --- /dev/null +++ b/wwwroot/today_usage.ui @@ -0,0 +1,32 @@ +{ + "widgettype": "VBox", + "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": "{{get_today_usage(request)}}", + "fontSize": "32px", + "fontWeight": "bold", + "color": "#1890ff" + } + } + ] +} diff --git a/wwwroot/top_models_chart.ui b/wwwroot/top_models_chart.ui new file mode 100644 index 0000000..9db03f1 --- /dev/null +++ b/wwwroot/top_models_chart.ui @@ -0,0 +1,10 @@ +{ + "widgettype": "ChartBar", + "options": { + "height": "300px", + "width": "100%", + "nameField": "model_name", + "valueFields": ["cnt", "total_amount"], + "data": {{get_top_models(request)|tojson}} + } +} diff --git a/wwwroot/total_users.ui b/wwwroot/total_users.ui new file mode 100644 index 0000000..5d8c86e --- /dev/null +++ b/wwwroot/total_users.ui @@ -0,0 +1,32 @@ +{ + "widgettype": "VBox", + "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": "{{get_total_users(request)}}", + "fontSize": "32px", + "fontWeight": "bold", + "color": "#722ed1" + } + } + ] +}