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
|
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
|
sql = """SELECT
|
||||||
COALESCE(currency, 'CNY') as currency,
|
COALESCE(currency, 'CNY') as currency,
|
||||||
COUNT(*) as call_count,
|
COUNT(*) as call_count,
|
||||||
COALESCE(SUM(amount), 0) as amount,
|
COALESCE(SUM(amount), 0) as amount,
|
||||||
COALESCE(SUM(amount_cny), 0) as amount_cny
|
COALESCE(SUM(amount_cny), 0) as amount_cny
|
||||||
FROM dm_model_call_fact
|
FROM dm_model_call_fact
|
||||||
WHERE 1=1
|
WHERE 1=1""" + w + """
|
||||||
GROUP BY dm_model_call_fact.currency
|
GROUP BY dm_model_call_fact.currency
|
||||||
ORDER BY call_count DESC"""
|
ORDER BY call_count DESC"""
|
||||||
recs = await sor.sqlExe(sql, {})
|
recs = await sor.sqlExe(sql, ns)
|
||||||
return [{
|
return [{
|
||||||
'currency': r.currency or 'CNY',
|
'currency': r.currency or 'CNY',
|
||||||
'call_count': int(r.call_count),
|
'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):
|
async def get_top_models(env, sor, limit=5, org_filter=None):
|
||||||
"""Top N 模型(按调用次数,全量)"""
|
"""Top N 模型(按调用次数,全量+可选org过滤)"""
|
||||||
sql = """SELECT model, COUNT(*) as cnt, COALESCE(SUM(amount_cny), 0) as total_amount
|
w = ""
|
||||||
FROM dm_model_call_fact
|
ns = {'limit': limit}
|
||||||
GROUP BY model ORDER BY cnt DESC LIMIT ${limit}$"""
|
if org_filter:
|
||||||
recs = await sor.sqlExe(sql, {'limit': limit})
|
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),
|
return [{'model_name': r.model or 'Unknown', 'cnt': int(r.cnt),
|
||||||
'total_amount': round(float(r.total_amount), 2)} for r in recs]
|
'total_amount': round(float(r.total_amount), 2)} for r in recs]
|
||||||
|
|
||||||
|
|
||||||
async def get_top_providers(env, sor, limit=5):
|
async def get_top_providers(env, sor, limit=5, org_filter=None):
|
||||||
"""Top N 供应商(按金额,全量)"""
|
"""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,
|
sql = """SELECT f.providerid, COUNT(*) as cnt, COALESCE(SUM(f.amount_cny), 0) as total_amount,
|
||||||
COALESCE(o.orgname, f.providerid) as provider_name
|
COALESCE(o.orgname, f.providerid) as provider_name
|
||||||
FROM dm_model_call_fact f
|
FROM dm_model_call_fact f
|
||||||
LEFT JOIN organization o ON o.id = f.providerid
|
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}$"""
|
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',
|
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]
|
'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):
|
async def get_top_users(env, sor, limit=5, org_filter=None):
|
||||||
"""全量 Top N 用户(按金额)"""
|
"""全量 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,
|
sql = """SELECT f.userid, COUNT(*) as cnt, COALESCE(SUM(f.amount_cny), 0) as total_amount,
|
||||||
COALESCE(u.username, f.userid) as user_name
|
COALESCE(u.username, f.userid) as user_name
|
||||||
FROM dm_model_call_fact f
|
FROM dm_model_call_fact f
|
||||||
LEFT JOIN users u ON u.id = f.userid
|
LEFT JOIN users u ON u.id = f.userid
|
||||||
|
WHERE 1=1""" + w + """
|
||||||
GROUP BY f.userid ORDER BY total_amount DESC LIMIT ${limit}$"""
|
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),
|
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]
|
'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):
|
async def api_top_models(request):
|
||||||
try:
|
try:
|
||||||
env = request._run_ns
|
env = request._run_ns
|
||||||
|
scope, org_filter = await _resolve_view(request)
|
||||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
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:
|
except Exception as e:
|
||||||
debug("sage_datamart API error: " + str(e))
|
debug("sage_datamart API error: " + str(e))
|
||||||
|
|
||||||
@ -134,8 +135,9 @@ async def api_top_models(request):
|
|||||||
async def api_top_providers(request):
|
async def api_top_providers(request):
|
||||||
try:
|
try:
|
||||||
env = request._run_ns
|
env = request._run_ns
|
||||||
|
scope, org_filter = await _resolve_view(request)
|
||||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
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:
|
except Exception as e:
|
||||||
debug("sage_datamart API error: " + str(e))
|
debug("sage_datamart API error: " + str(e))
|
||||||
|
|
||||||
@ -145,8 +147,9 @@ async def api_top_providers(request):
|
|||||||
async def api_top_users(request):
|
async def api_top_users(request):
|
||||||
try:
|
try:
|
||||||
env = request._run_ns
|
env = request._run_ns
|
||||||
|
scope, org_filter = await _resolve_view(request)
|
||||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
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:
|
except Exception as e:
|
||||||
debug("sage_datamart API error: " + str(e))
|
debug("sage_datamart API error: " + str(e))
|
||||||
|
|
||||||
@ -193,8 +196,9 @@ async def api_hourly_concurrency(request):
|
|||||||
async def api_currency_stats(request):
|
async def api_currency_stats(request):
|
||||||
try:
|
try:
|
||||||
env = request._run_ns
|
env = request._run_ns
|
||||||
|
scope, org_filter = await _resolve_view(request)
|
||||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
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:
|
except Exception as e:
|
||||||
debug("sage_datamart API error: " + str(e))
|
debug("sage_datamart API error: " + str(e))
|
||||||
|
|
||||||
@ -404,6 +408,26 @@ async def j2_avg_ttft(request):
|
|||||||
return 0
|
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 加载 ──
|
# ── 监控组件 j2 函数,每段独立 .ui 通过 RefreshWidget 加载 ──
|
||||||
|
|
||||||
async def j2_model_perf_data(request):
|
async def j2_model_perf_data(request):
|
||||||
@ -519,6 +543,7 @@ def load_sage_datamart():
|
|||||||
env.j2_online_users = j2_online_users
|
env.j2_online_users = j2_online_users
|
||||||
env.j2_scoped_success_rate = j2_scoped_success_rate
|
env.j2_scoped_success_rate = j2_scoped_success_rate
|
||||||
env.j2_scoped_fail_count = j2_scoped_fail_count
|
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_model_perf_data = j2_model_perf_data
|
||||||
env.j2_currency_data = j2_currency_data
|
env.j2_currency_data = j2_currency_data
|
||||||
env.j2_provider_roi_data = j2_provider_roi_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")
|
PYTHON = os.path.join(SAGE_ROOT, "py3", "bin", "python")
|
||||||
SET_PERM = os.path.join(SAGE_ROOT, "set_role_perm.py")
|
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 = [
|
PATHS_ANY = [
|
||||||
f"/{MOD}/cron/etl_sync.dspy",
|
f"/{MOD}/cron/etl_sync.dspy",
|
||||||
f"/{MOD}/cron/etl_aggregate.dspy",
|
f"/{MOD}/cron/etl_aggregate.dspy",
|
||||||
f"/{MOD}/cron/etl_provider_cost.dspy",
|
f"/{MOD}/cron/etl_provider_cost.dspy",
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
PATHS_LOGINED = [
|
PATHS_LOGINED = [
|
||||||
@ -41,6 +39,7 @@ PATHS_LOGINED = [
|
|||||||
f"/{MOD}/sect_top_models.ui",
|
f"/{MOD}/sect_top_models.ui",
|
||||||
f"/{MOD}/sect_top_users.ui",
|
f"/{MOD}/sect_top_users.ui",
|
||||||
f"/{MOD}/sect_top_providers.ui",
|
f"/{MOD}/sect_top_providers.ui",
|
||||||
|
f"/{MOD}/sect_user_apikeys.ui",
|
||||||
]
|
]
|
||||||
|
|
||||||
def run(role, path):
|
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"},
|
"options": {"css": "card", "width": "50%", "padding": "16px"},
|
||||||
"subwidgets": [
|
"subwidgets": [
|
||||||
{"widgettype": "Title4", "options": {"fontWeight": "600", "otext": "我的模型(按金额Top5)", "i18n": true, "marginBottom": "8px"}},
|
{"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"},
|
"options": {"css": "card", "width": "50%", "padding": "16px"},
|
||||||
"subwidgets": [
|
"subwidgets": [
|
||||||
{"widgettype": "Title4", "options": {"fontWeight": "600", "otext": "API Key排行", "i18n": true, "marginBottom": "8px"}},
|
{"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