feat: 客户视图(隐藏用户区+org过滤图表+API Key排行端点)
This commit is contained in:
parent
85afc1f023
commit
59072ef035
@ -212,18 +212,23 @@ async def get_avg_ttft(env, sor):
|
||||
return 0
|
||||
|
||||
|
||||
async def get_currency_breakdown(env, sor):
|
||||
"""当天各币种分布"""
|
||||
async def get_currency_breakdown(env, sor, org_filter=None):
|
||||
"""各币种分布(全量+可选org过滤)"""
|
||||
w = ""
|
||||
ns = {}
|
||||
if org_filter:
|
||||
w = " AND userorgid = ${org}$"
|
||||
ns['org'] = org_filter
|
||||
sql = """SELECT
|
||||
COALESCE(currency, 'CNY') as currency,
|
||||
COUNT(*) as call_count,
|
||||
COALESCE(SUM(amount), 0) as amount,
|
||||
COALESCE(SUM(amount_cny), 0) as amount_cny
|
||||
FROM dm_model_call_fact
|
||||
WHERE 1=1
|
||||
WHERE 1=1""" + w + """
|
||||
GROUP BY dm_model_call_fact.currency
|
||||
ORDER BY call_count DESC"""
|
||||
recs = await sor.sqlExe(sql, {})
|
||||
recs = await sor.sqlExe(sql, ns)
|
||||
return [{
|
||||
'currency': r.currency or 'CNY',
|
||||
'call_count': int(r.call_count),
|
||||
@ -234,37 +239,53 @@ async def get_currency_breakdown(env, sor):
|
||||
|
||||
# ── 排行榜 ──
|
||||
|
||||
async def get_top_models(env, sor, limit=5):
|
||||
"""Top N 模型(按调用次数,全量)"""
|
||||
sql = """SELECT model, COUNT(*) as cnt, COALESCE(SUM(amount_cny), 0) as total_amount
|
||||
FROM dm_model_call_fact
|
||||
GROUP BY model ORDER BY cnt DESC LIMIT ${limit}$"""
|
||||
recs = await sor.sqlExe(sql, {'limit': limit})
|
||||
async def get_top_models(env, sor, limit=5, org_filter=None):
|
||||
"""Top N 模型(按调用次数,全量+可选org过滤)"""
|
||||
w = ""
|
||||
ns = {'limit': limit}
|
||||
if org_filter:
|
||||
w = " AND userorgid = ${org}$"
|
||||
ns['org'] = org_filter
|
||||
sql = ("SELECT model, COUNT(*) as cnt, COALESCE(SUM(amount_cny), 0) as total_amount "
|
||||
"FROM dm_model_call_fact WHERE 1=1" + w +
|
||||
" GROUP BY model ORDER BY cnt DESC LIMIT ${limit}$")
|
||||
recs = await sor.sqlExe(sql, ns)
|
||||
return [{'model_name': r.model or 'Unknown', 'cnt': int(r.cnt),
|
||||
'total_amount': round(float(r.total_amount), 2)} for r in recs]
|
||||
|
||||
|
||||
async def get_top_providers(env, sor, limit=5):
|
||||
"""Top N 供应商(按金额,全量)"""
|
||||
async def get_top_providers(env, sor, limit=5, org_filter=None):
|
||||
"""Top N 供应商(按金额,全量+可选org过滤)"""
|
||||
w = ""
|
||||
ns = {'limit': limit}
|
||||
if org_filter:
|
||||
w = " AND f.userorgid = ${org}$"
|
||||
ns['org'] = org_filter
|
||||
sql = """SELECT f.providerid, COUNT(*) as cnt, COALESCE(SUM(f.amount_cny), 0) as total_amount,
|
||||
COALESCE(o.orgname, f.providerid) as provider_name
|
||||
FROM dm_model_call_fact f
|
||||
LEFT JOIN organization o ON o.id = f.providerid
|
||||
WHERE f.providerid IS NOT NULL
|
||||
WHERE f.providerid IS NOT NULL""" + w + """
|
||||
GROUP BY f.providerid ORDER BY total_amount DESC LIMIT ${limit}$"""
|
||||
recs = await sor.sqlExe(sql, {'limit': limit})
|
||||
recs = await sor.sqlExe(sql, ns)
|
||||
return [{'provider_name': r.provider_name or r.providerid or 'Unknown',
|
||||
'cnt': int(r.cnt), 'total_amount': round(float(r.total_amount), 2)} for r in recs]
|
||||
|
||||
|
||||
async def get_top_users(env, sor, limit=5):
|
||||
"""全量 Top N 用户(按金额)"""
|
||||
async def get_top_users(env, sor, limit=5, org_filter=None):
|
||||
"""全量 Top N 用户(按金额+可选org过滤)"""
|
||||
w = ""
|
||||
ns = {'limit': limit}
|
||||
if org_filter:
|
||||
w = " AND f.userorgid = ${org}$"
|
||||
ns['org'] = org_filter
|
||||
sql = """SELECT f.userid, COUNT(*) as cnt, COALESCE(SUM(f.amount_cny), 0) as total_amount,
|
||||
COALESCE(u.username, f.userid) as user_name
|
||||
FROM dm_model_call_fact f
|
||||
LEFT JOIN users u ON u.id = f.userid
|
||||
WHERE 1=1""" + w + """
|
||||
GROUP BY f.userid ORDER BY total_amount DESC LIMIT ${limit}$"""
|
||||
recs = await sor.sqlExe(sql, {'limit': limit})
|
||||
recs = await sor.sqlExe(sql, ns)
|
||||
return [{'user_name': r.user_name or r.userid, 'cnt': int(r.cnt),
|
||||
'total_amount': round(float(r.total_amount), 2)} for r in recs]
|
||||
|
||||
|
||||
@ -123,8 +123,9 @@ async def api_stats(request):
|
||||
async def api_top_models(request):
|
||||
try:
|
||||
env = request._run_ns
|
||||
scope, org_filter = await _resolve_view(request)
|
||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
||||
return await get_top_models(env, sor)
|
||||
return await get_top_models(env, sor, org_filter=org_filter)
|
||||
except Exception as e:
|
||||
debug("sage_datamart API error: " + str(e))
|
||||
|
||||
@ -134,8 +135,9 @@ async def api_top_models(request):
|
||||
async def api_top_providers(request):
|
||||
try:
|
||||
env = request._run_ns
|
||||
scope, org_filter = await _resolve_view(request)
|
||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
||||
return await get_top_providers(env, sor)
|
||||
return await get_top_providers(env, sor, org_filter=org_filter)
|
||||
except Exception as e:
|
||||
debug("sage_datamart API error: " + str(e))
|
||||
|
||||
@ -145,8 +147,9 @@ async def api_top_providers(request):
|
||||
async def api_top_users(request):
|
||||
try:
|
||||
env = request._run_ns
|
||||
scope, org_filter = await _resolve_view(request)
|
||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
||||
return await get_top_users(env, sor)
|
||||
return await get_top_users(env, sor, org_filter=org_filter)
|
||||
except Exception as e:
|
||||
debug("sage_datamart API error: " + str(e))
|
||||
|
||||
@ -193,8 +196,9 @@ async def api_hourly_concurrency(request):
|
||||
async def api_currency_stats(request):
|
||||
try:
|
||||
env = request._run_ns
|
||||
scope, org_filter = await _resolve_view(request)
|
||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
||||
return await get_currency_breakdown(env, sor)
|
||||
return await get_currency_breakdown(env, sor, org_filter=org_filter)
|
||||
except Exception as e:
|
||||
debug("sage_datamart API error: " + str(e))
|
||||
|
||||
@ -404,6 +408,26 @@ async def j2_avg_ttft(request):
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
async def api_user_apikeys(request):
|
||||
"""当前用户每个 API Key 的使用排行"""
|
||||
try:
|
||||
env = request._run_ns
|
||||
userid = await env.get_userid()
|
||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
||||
sql = """SELECT uak.name as apikey_name, COUNT(*) as cnt, COALESCE(SUM(lu.amount), 0) as total_amount
|
||||
FROM llmusage lu
|
||||
JOIN userapikey uak ON uak.apikey = lu.apikey
|
||||
WHERE lu.userid = ${userid}$
|
||||
GROUP BY uak.name ORDER BY cnt DESC LIMIT 10"""
|
||||
recs = await sor.sqlExe(sql, {'userid': userid})
|
||||
return [{'apikey_name': r.apikey_name or 'Unknown',
|
||||
'cnt': int(r.cnt or 0),
|
||||
'total_amount': round(float(r.total_amount or 0), 2)} for r in recs]
|
||||
except Exception as e:
|
||||
debug("sage_datamart API error: " + str(e))
|
||||
return []
|
||||
|
||||
# ── 监控组件 j2 函数,每段独立 .ui 通过 RefreshWidget 加载 ──
|
||||
|
||||
async def j2_model_perf_data(request):
|
||||
@ -519,6 +543,7 @@ def load_sage_datamart():
|
||||
env.j2_online_users = j2_online_users
|
||||
env.j2_scoped_success_rate = j2_scoped_success_rate
|
||||
env.j2_scoped_fail_count = j2_scoped_fail_count
|
||||
env.api_user_apikeys = api_user_apikeys
|
||||
env.j2_model_perf_data = j2_model_perf_data
|
||||
env.j2_currency_data = j2_currency_data
|
||||
env.j2_provider_roi_data = j2_provider_roi_data
|
||||
|
||||
@ -7,12 +7,10 @@ SAGE_ROOT = os.path.expanduser("~/sage")
|
||||
PYTHON = os.path.join(SAGE_ROOT, "py3", "bin", "python")
|
||||
SET_PERM = os.path.join(SAGE_ROOT, "set_role_perm.py")
|
||||
|
||||
# cron endpoints — called via curl from localhost crontab, IP-checked in dspy
|
||||
PATHS_ANY = [
|
||||
f"/{MOD}/cron/etl_sync.dspy",
|
||||
f"/{MOD}/cron/etl_aggregate.dspy",
|
||||
f"/{MOD}/cron/etl_provider_cost.dspy",
|
||||
|
||||
]
|
||||
|
||||
PATHS_LOGINED = [
|
||||
@ -41,6 +39,7 @@ PATHS_LOGINED = [
|
||||
f"/{MOD}/sect_top_models.ui",
|
||||
f"/{MOD}/sect_top_users.ui",
|
||||
f"/{MOD}/sect_top_providers.ui",
|
||||
f"/{MOD}/sect_user_apikeys.ui",
|
||||
]
|
||||
|
||||
def run(role, path):
|
||||
|
||||
5
wwwroot/api/user_apikeys.dspy
Normal file
5
wwwroot/api/user_apikeys.dspy
Normal file
@ -0,0 +1,5 @@
|
||||
# API Key 使用排行
|
||||
import json
|
||||
env = request._run_ns
|
||||
data = await env.api_user_apikeys(request)
|
||||
return json.dumps(data, ensure_ascii=False)
|
||||
@ -56,7 +56,7 @@
|
||||
"options": {"css": "card", "width": "50%", "padding": "16px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Title4", "options": {"fontWeight": "600", "otext": "我的模型(按金额Top5)", "i18n": true, "marginBottom": "8px"}},
|
||||
{"widgettype": "RefreshWidget", "options": {"period_seconds": 60, "url": "{{entire_url('sect_top_models.ui')}}", "width": "100%"}}
|
||||
{"widgettype": "RefreshWidget", "options": {"period_seconds": 60, "url": "{{entire_url('sect_user_apikeys.ui')}}", "width": "100%"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -64,7 +64,7 @@
|
||||
"options": {"css": "card", "width": "50%", "padding": "16px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Title4", "options": {"fontWeight": "600", "otext": "API Key排行", "i18n": true, "marginBottom": "8px"}},
|
||||
{"widgettype": "RefreshWidget", "options": {"period_seconds": 60, "url": "{{entire_url('sect_top_models.ui')}}", "width": "100%"}}
|
||||
{"widgettype": "RefreshWidget", "options": {"period_seconds": 60, "url": "{{entire_url('sect_user_apikeys.ui')}}", "width": "100%"}}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
9
wwwroot/sect_user_apikeys.ui
Normal file
9
wwwroot/sect_user_apikeys.ui
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"widgettype": "ChartBar",
|
||||
"options": {
|
||||
"height": "200px",
|
||||
"data_url": "/sage_datamart/api/user_apikeys.dspy",
|
||||
"nameField": "apikey_name",
|
||||
"valueFields": ["cnt"]
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user