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": {