diff --git a/dashboard_for_sage/load_dashboard.py b/dashboard_for_sage/load_dashboard.py index 71ca46e..942a106 100644 --- a/dashboard_for_sage/load_dashboard.py +++ b/dashboard_for_sage/load_dashboard.py @@ -269,6 +269,178 @@ async def get_user_today_models(request): return result +# ── Customer (org-level) monitoring functions ── + + +async def _get_org_id(request): + """Helper: get current user's org_id from request context.""" + env = request._run_ns + org_id = await env.get_userorgid() + return org_id or '0' + + +async def get_customer_daily_models(request): + """获取当前客户组织当天各模型调用次数和金额""" + env = request._run_ns + org_id = await _get_org_id(request) + today = env.curDateString() + async with get_sor_context(env, 'sage') as sor: + sql = """ + SELECT + COALESCE(b.name, 'Unknown') 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}$ + AND a.userorgid = ${org_id}$ + GROUP BY a.llmid, b.name + ORDER BY cnt DESC + """ + recs = await sor.sqlExe(sql, {'today': today, 'org_id': org_id}) + result = [] + for r in recs: + result.append({ + 'model_name': r.get('model_name', 'Unknown'), + 'cnt': int(r.get('cnt', 0)), + 'total_amount': round(float(r.get('total_amount', 0)), 4) + }) + return result + + +async def get_customer_monthly_models(request): + """获取当前客户组织当月各模型调用次数和金额""" + env = request._run_ns + org_id = await _get_org_id(request) + now = datetime.now() + month_start = now.strftime('%Y-%m-01') + if now.month == 12: + month_end = f'{now.year + 1}-01-01' + else: + month_end = f'{now.year}-{now.month + 1:02d}-01' + async with get_sor_context(env, 'sage') as sor: + sql = """ + SELECT + COALESCE(b.name, 'Unknown') 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 >= ${month_start}$ + AND a.use_date < ${month_end}$ + AND a.userorgid = ${org_id}$ + GROUP BY a.llmid, b.name + ORDER BY cnt DESC + """ + recs = await sor.sqlExe(sql, { + 'month_start': month_start, + 'month_end': month_end, + 'org_id': org_id + }) + result = [] + for r in recs: + result.append({ + 'model_name': r.get('model_name', 'Unknown'), + 'cnt': int(r.get('cnt', 0)), + 'total_amount': round(float(r.get('total_amount', 0)), 4) + }) + return result + + +async def get_customer_daily_summary(request): + """获取当前客户组织今日汇总(调用次数+金额)""" + env = request._run_ns + org_id = await _get_org_id(request) + today = env.curDateString() + async with get_sor_context(env, 'sage') as sor: + sql = """ + SELECT + COUNT(*) as cnt, + COALESCE(SUM(a.amount), 0) as total_amount + FROM llmusage a + WHERE a.use_date = ${today}$ + AND a.userorgid = ${org_id}$ + """ + recs = await sor.sqlExe(sql, {'today': today, 'org_id': org_id}) + if recs: + return { + 'cnt': int(recs[0].get('cnt', 0)), + 'total_amount': round(float(recs[0].get('total_amount', 0)), 4) + } + return {'cnt': 0, 'total_amount': 0} + + +async def get_customer_month_summary(request): + """获取当前客户组织当月汇总(调用次数+金额)""" + env = request._run_ns + org_id = await _get_org_id(request) + now = datetime.now() + month_start = now.strftime('%Y-%m-01') + if now.month == 12: + month_end = f'{now.year + 1}-01-01' + else: + month_end = f'{now.year}-{now.month + 1:02d}-01' + async with get_sor_context(env, 'sage') as sor: + sql = """ + SELECT + COUNT(*) as cnt, + COALESCE(SUM(a.amount), 0) as total_amount + FROM llmusage a + WHERE a.use_date >= ${month_start}$ + AND a.use_date < ${month_end}$ + AND a.userorgid = ${org_id}$ + """ + recs = await sor.sqlExe(sql, { + 'month_start': month_start, + 'month_end': month_end, + 'org_id': org_id + }) + if recs: + return { + 'cnt': int(recs[0].get('cnt', 0)), + 'total_amount': round(float(recs[0].get('total_amount', 0)), 4) + } + return {'cnt': 0, 'total_amount': 0} + + +async def get_customer_daily_trend(request): + """获取当前客户组织当月每日调用趋势(每天的调用次数和金额)""" + env = request._run_ns + org_id = await _get_org_id(request) + now = datetime.now() + month_start = now.strftime('%Y-%m-01') + if now.month == 12: + month_end = f'{now.year + 1}-01-01' + else: + month_end = f'{now.year}-{now.month + 1:02d}-01' + async with get_sor_context(env, 'sage') as sor: + sql = """ + SELECT + a.use_date as date, + COUNT(*) as cnt, + COALESCE(SUM(a.amount), 0) as total_amount + FROM llmusage a + WHERE a.use_date >= ${month_start}$ + AND a.use_date < ${month_end}$ + AND a.userorgid = ${org_id}$ + GROUP BY a.use_date + ORDER BY a.use_date ASC + """ + recs = await sor.sqlExe(sql, { + 'month_start': month_start, + 'month_end': month_end, + 'org_id': org_id + }) + result = [] + for r in recs: + result.append({ + 'date': r.get('date', ''), + 'cnt': int(r.get('cnt', 0)), + 'total_amount': round(float(r.get('total_amount', 0)), 4) + }) + return result + + def load_dashboard(): """Register dashboard functions on ServerEnv""" g = ServerEnv() @@ -286,3 +458,8 @@ def load_dashboard(): g.get_new_users_month = get_new_users_month g.get_total_orgs = get_total_orgs g.get_user_today_models = get_user_today_models + g.get_customer_daily_models = get_customer_daily_models + g.get_customer_monthly_models = get_customer_monthly_models + g.get_customer_daily_summary = get_customer_daily_summary + g.get_customer_month_summary = get_customer_month_summary + g.get_customer_daily_trend = get_customer_daily_trend diff --git a/scripts/load_path.py b/scripts/load_path.py index f51e66a..48ff141 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -69,9 +69,18 @@ paths = [ ("/dashboard_for_sage/top_models_chart.ui", "logined"), ("/dashboard_for_sage/user_today_models_chart.ui", "logined"), + # Customer monitoring + ("/dashboard_for_sage/customer_usage.ui", "logined"), + ("/dashboard_for_sage/customer_daily_chart.ui", "logined"), + ("/dashboard_for_sage/customer_monthly_chart.ui", "logined"), + ("/dashboard_for_sage/customer_daily_trend.ui", "logined"), + # API endpoints ("/dashboard_for_sage/api/top_models.dspy", "logined"), ("/dashboard_for_sage/api/user_today_models.dspy", "logined"), + ("/dashboard_for_sage/api/customer_daily_models.dspy", "logined"), + ("/dashboard_for_sage/api/customer_monthly_models.dspy", "logined"), + ("/dashboard_for_sage/api/customer_daily_trend.dspy", "logined"), ] diff --git a/wwwroot/api/customer_daily_models.dspy b/wwwroot/api/customer_daily_models.dspy new file mode 100644 index 0000000..e2b18b5 --- /dev/null +++ b/wwwroot/api/customer_daily_models.dspy @@ -0,0 +1,6 @@ +# coding=utf-8 +"""Customer daily per-model usage data API for ChartBar""" +import json + +models = await get_customer_daily_models(request) +return json.dumps(models, ensure_ascii=False, default=str) diff --git a/wwwroot/api/customer_daily_trend.dspy b/wwwroot/api/customer_daily_trend.dspy new file mode 100644 index 0000000..da45ec9 --- /dev/null +++ b/wwwroot/api/customer_daily_trend.dspy @@ -0,0 +1,6 @@ +# coding=utf-8 +"""Customer daily trend data API for ChartLine (current month)""" +import json + +trend = await get_customer_daily_trend(request) +return json.dumps(trend, ensure_ascii=False, default=str) diff --git a/wwwroot/api/customer_monthly_models.dspy b/wwwroot/api/customer_monthly_models.dspy new file mode 100644 index 0000000..8a39edf --- /dev/null +++ b/wwwroot/api/customer_monthly_models.dspy @@ -0,0 +1,6 @@ +# coding=utf-8 +"""Customer monthly per-model usage data API for ChartBar""" +import json + +models = await get_customer_monthly_models(request) +return json.dumps(models, ensure_ascii=False, default=str) diff --git a/wwwroot/customer_daily_chart.ui b/wwwroot/customer_daily_chart.ui new file mode 100644 index 0000000..5fd932c --- /dev/null +++ b/wwwroot/customer_daily_chart.ui @@ -0,0 +1,11 @@ +{ + "widgettype": "ChartBar", + "options": { + "height": "300px", + "width": "100%", + "data_url": "{{entire_url('api/customer_daily_models.dspy')}}", + "nameField": "model_name", + "valueFields": ["cnt", "total_amount"], + "refresh_period": 60 + } +} diff --git a/wwwroot/customer_daily_trend.ui b/wwwroot/customer_daily_trend.ui new file mode 100644 index 0000000..e196cb2 --- /dev/null +++ b/wwwroot/customer_daily_trend.ui @@ -0,0 +1,11 @@ +{ + "widgettype": "ChartLine", + "options": { + "height": "280px", + "width": "100%", + "data_url": "{{entire_url('api/customer_daily_trend.dspy')}}", + "nameField": "date", + "valueFields": ["cnt", "total_amount"], + "refresh_period": 120 + } +} diff --git a/wwwroot/customer_monthly_chart.ui b/wwwroot/customer_monthly_chart.ui new file mode 100644 index 0000000..6539bb0 --- /dev/null +++ b/wwwroot/customer_monthly_chart.ui @@ -0,0 +1,11 @@ +{ + "widgettype": "ChartBar", + "options": { + "height": "300px", + "width": "100%", + "data_url": "{{entire_url('api/customer_monthly_models.dspy')}}", + "nameField": "model_name", + "valueFields": ["cnt", "total_amount"], + "refresh_period": 120 + } +} diff --git a/wwwroot/customer_usage.ui b/wwwroot/customer_usage.ui new file mode 100644 index 0000000..05acf05 --- /dev/null +++ b/wwwroot/customer_usage.ui @@ -0,0 +1,462 @@ +{ + "widgettype": "VBox", + "options": { + "width": "100%", + "height": "100%" + }, + "subwidgets": [ + { + "widgettype": "VScrollPanel", + "options": { + "css": "filler" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "width": "100%", + "alignItems": "center", + "marginBottom": "24px" + }, + "subwidgets": [ + { + "widgettype": "Title2", + "options": { + "fontWeight": "700", + "otext": "客户专属监控", + "i18n": true + } + }, + { + "widgettype": "Filler" + }, + { + "widgettype": "Button", + "options": { + "label": "返回首页", + "borderRadius": "6px", + "padding": "6px 16px", + "fontSize": "13px" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "app.sage_main_content", + "options": { + "url": "{{entire_url('/dashboard_for_sage/index.ui')}}" + }, + "mode": "replace" + } + ] + } + ] + }, + { + "widgettype": "ResponsableBox", + "options": { + "gap": "16px", + "minWidth": "220px", + "marginBottom": "24px" + }, + "subwidgets": [ + { + "widgettype": "VBox", + "options": { + "css": "stat-card", + "padding": "20px", + "borderRadius": "12px", + "flex": "1", + "minHeight": "110px" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "alignItems": "center", + "marginBottom": "12px" + }, + "subwidgets": [ + { + "widgettype": "Svg", + "options": { + "svg": "", + "width": "24px", + "height": "24px" + } + }, + { + "widgettype": "Filler" + } + ] + }, + { + "widgettype": "Text", + "options": { + "css": "stat-value", + "text": "{{get_customer_daily_summary(request).cnt}}", + "fontSize": "32px", + "fontWeight": "700", + "lineHeight": "1.1" + } + }, + { + "widgettype": "Text", + "options": { + "css": "stat-label", + "fontSize": "14px", + "marginTop": "4px", + "otext": "今日调用次数", + "i18n": true + } + } + ] + }, + { + "widgettype": "VBox", + "options": { + "css": "stat-card", + "padding": "20px", + "borderRadius": "12px", + "flex": "1", + "minHeight": "110px" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "alignItems": "center", + "marginBottom": "12px" + }, + "subwidgets": [ + { + "widgettype": "Svg", + "options": { + "svg": "", + "width": "24px", + "height": "24px" + } + }, + { + "widgettype": "Filler" + } + ] + }, + { + "widgettype": "Text", + "options": { + "css": "stat-value", + "text": "¥{{get_customer_daily_summary(request).total_amount|round(2)}}", + "fontSize": "32px", + "fontWeight": "700", + "lineHeight": "1.1" + } + }, + { + "widgettype": "Text", + "options": { + "css": "stat-label", + "fontSize": "14px", + "marginTop": "4px", + "otext": "今日消费金额", + "i18n": true + } + } + ] + }, + { + "widgettype": "VBox", + "options": { + "css": "stat-card", + "padding": "20px", + "borderRadius": "12px", + "flex": "1", + "minHeight": "110px" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "alignItems": "center", + "marginBottom": "12px" + }, + "subwidgets": [ + { + "widgettype": "Svg", + "options": { + "svg": "", + "width": "24px", + "height": "24px" + } + }, + { + "widgettype": "Filler" + } + ] + }, + { + "widgettype": "Text", + "options": { + "css": "stat-value", + "text": "{{get_customer_month_summary(request).cnt}}", + "fontSize": "32px", + "fontWeight": "700", + "lineHeight": "1.1" + } + }, + { + "widgettype": "Text", + "options": { + "css": "stat-label", + "fontSize": "14px", + "marginTop": "4px", + "otext": "本月调用次数", + "i18n": true + } + } + ] + }, + { + "widgettype": "VBox", + "options": { + "css": "stat-card", + "padding": "20px", + "borderRadius": "12px", + "flex": "1", + "minHeight": "110px" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "alignItems": "center", + "marginBottom": "12px" + }, + "subwidgets": [ + { + "widgettype": "Svg", + "options": { + "svg": "", + "width": "24px", + "height": "24px" + } + }, + { + "widgettype": "Filler" + } + ] + }, + { + "widgettype": "Text", + "options": { + "css": "stat-value", + "text": "¥{{get_customer_month_summary(request).total_amount|round(2)}}", + "fontSize": "32px", + "fontWeight": "700", + "lineHeight": "1.1" + } + }, + { + "widgettype": "Text", + "options": { + "css": "stat-label", + "fontSize": "14px", + "marginTop": "4px", + "otext": "本月消费金额", + "i18n": true + } + } + ] + } + ] + }, + { + "widgettype": "VBox", + "options": { + "css": "card", + "width": "100%", + "borderRadius": "12px", + "padding": "20px", + "marginBottom": "20px" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "width": "100%", + "alignItems": "center", + "marginBottom": "16px" + }, + "subwidgets": [ + { + "widgettype": "Title4", + "options": { + "fontWeight": "600", + "otext": "本月每日调用趋势", + "i18n": true + } + }, + { + "widgettype": "Filler" + }, + { + "widgettype": "Button", + "options": { + "label": "刷新", + "border": "none", + "borderRadius": "6px", + "padding": "4px 12px", + "fontSize": "12px" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "method", + "target": "-@ChartLine", + "method": "render_urldata", + "params": {} + } + ] + } + ] + }, + { + "widgettype": "urlwidget", + "options": { + "url": "{{entire_url('customer_daily_trend.ui')}}" + } + } + ] + }, + { + "widgettype": "HBox", + "options": { + "width": "100%", + "gap": "20px", + "marginBottom": "20px" + }, + "subwidgets": [ + { + "widgettype": "VBox", + "options": { + "css": "card", + "width": "50%", + "borderRadius": "12px", + "padding": "20px" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "width": "100%", + "alignItems": "center", + "marginBottom": "16px" + }, + "subwidgets": [ + { + "widgettype": "Title4", + "options": { + "fontWeight": "600", + "otext": "今日各模型调用", + "i18n": true + } + }, + { + "widgettype": "Filler" + }, + { + "widgettype": "Button", + "options": { + "label": "刷新", + "border": "none", + "borderRadius": "6px", + "padding": "4px 12px", + "fontSize": "12px" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "method", + "target": "-@ChartBar", + "method": "render_urldata", + "params": {} + } + ] + } + ] + }, + { + "widgettype": "urlwidget", + "options": { + "url": "{{entire_url('customer_daily_chart.ui')}}" + } + } + ] + }, + { + "widgettype": "VBox", + "options": { + "css": "card", + "width": "50%", + "borderRadius": "12px", + "padding": "20px" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "width": "100%", + "alignItems": "center", + "marginBottom": "16px" + }, + "subwidgets": [ + { + "widgettype": "Title4", + "options": { + "fontWeight": "600", + "otext": "本月各模型调用", + "i18n": true + } + }, + { + "widgettype": "Filler" + }, + { + "widgettype": "Button", + "options": { + "label": "刷新", + "border": "none", + "borderRadius": "6px", + "padding": "4px 12px", + "fontSize": "12px" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "method", + "target": "-@ChartBar", + "method": "render_urldata", + "params": {} + } + ] + } + ] + }, + { + "widgettype": "urlwidget", + "options": { + "url": "{{entire_url('customer_monthly_chart.ui')}}" + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/wwwroot/index.ui b/wwwroot/index.ui index 9e7ab27..a5e2971 100644 --- a/wwwroot/index.ui +++ b/wwwroot/index.ui @@ -471,6 +471,82 @@ } ] }, + { + "widgettype": "VBox", + "options": { + "css": "card", + "width": "100%", + "borderRadius": "12px", + "padding": "20px", + "marginTop": "20px", + "cursor": "pointer" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "app.sage_main_content", + "options": { + "url": "{{entire_url('/dashboard_for_sage/customer_usage.ui')}}" + }, + "mode": "replace" + } + ], + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "width": "100%", + "alignItems": "center" + }, + "subwidgets": [ + { + "widgettype": "Svg", + "options": { + "svg": "", + "width": "28px", + "height": "28px", + "marginRight": "12px" + } + }, + { + "widgettype": "VBox", + "options": {}, + "subwidgets": [ + { + "widgettype": "Title4", + "options": { + "fontWeight": "600", + "otext": "客户专属监控", + "i18n": true + } + }, + { + "widgettype": "Text", + "options": { + "fontSize": "13px", + "otext": "查看本组织各模型每日/每月调用次数与金额统计", + "i18n": true + } + } + ] + }, + { + "widgettype": "Filler" + }, + { + "widgettype": "Svg", + "options": { + "svg": "", + "width": "24px", + "height": "24px" + } + } + ] + } + ] + }, { "widgettype": "VBox", "options": {