commit 4d24094048aae7544872d3d99ded83d8e61b9ec7 Author: yumoqing Date: Fri Jul 17 16:56:52 2026 +0800 feat: data_mart module — model perf monitoring + provider ROI dashboard diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f7983e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.pyc +*.egg-info/ diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..a3b2883 --- /dev/null +++ b/build.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +cd "$(dirname "$0")" +echo "[data_mart] build done" diff --git a/data_mart/__init__.py b/data_mart/__init__.py new file mode 100644 index 0000000..74211af --- /dev/null +++ b/data_mart/__init__.py @@ -0,0 +1,2 @@ +"""data_mart — Sage 数据集市模块""" +from .init import load_data_mart diff --git a/data_mart/etl.py b/data_mart/etl.py new file mode 100644 index 0000000..17b4523 --- /dev/null +++ b/data_mart/etl.py @@ -0,0 +1,286 @@ +""" +数据集市 ETL — 增量同步 llmusage → 集市表 +""" +import json +import asyncio +from datetime import datetime, timedelta +from appPublic.uniqueID import getID +from appPublic.dictObject import DictObject +from appPublic.jsonConfig import getConfig +from appPublic.log import debug, info, exception +from sqlor.dbpools import DBPools +from ahserver.serverenv import ServerEnv + +MODULE_NAME = "data_mart" + + +async def ensure_tables(sor): + """创建集市表(如不存在)""" + tables = [ + """CREATE TABLE IF NOT EXISTS dm_model_call_fact ( + id VARCHAR(32) PRIMARY KEY, + luid VARCHAR(32) NOT NULL, + llmid VARCHAR(32) NOT NULL, + model VARCHAR(100), + catelogid VARCHAR(10), + userid VARCHAR(32) NOT NULL, + userorgid VARCHAR(32) NOT NULL, + ownerid VARCHAR(32), + providerid VARCHAR(32), + call_date DATE NOT NULL, + call_hour TINYINT NOT NULL, + call_time DATETIME NOT NULL, + ttft_ms INT, + ttot_ms INT, + prompt_tokens INT DEFAULT 0, + completion_tokens INT DEFAULT 0, + status VARCHAR(20), + fail_reason VARCHAR(200), + amount DECIMAL(10,4) DEFAULT 0, + currency VARCHAR(5) DEFAULT 'CNY', + distributor_orgid VARCHAR(32), + sale_userid VARCHAR(32), + created_at DATETIME DEFAULT NOW(), + INDEX idx_call_date (call_date), + INDEX idx_llmid (llmid), + INDEX idx_userorgid (userorgid), + INDEX idx_ownerid (ownerid), + INDEX idx_status (status), + INDEX idx_model (model) + )""", + + """CREATE TABLE IF NOT EXISTS dm_model_perf_daily ( + id VARCHAR(32) PRIMARY KEY, + stat_date DATE NOT NULL, + llmid VARCHAR(32) NOT NULL, + model VARCHAR(100), + catelogid VARCHAR(10), + providerid VARCHAR(32), + ownerid VARCHAR(32), + total_calls INT DEFAULT 0, + success_calls INT DEFAULT 0, + fail_calls INT DEFAULT 0, + success_rate DECIMAL(5,2), + avg_ttft_ms DECIMAL(10,1), + p50_ttft_ms DECIMAL(10,1), + p95_ttft_ms DECIMAL(10,1), + p99_ttft_ms DECIMAL(10,1), + avg_ttot_ms DECIMAL(10,1), + p50_ttot_ms DECIMAL(10,1), + p95_ttot_ms DECIMAL(10,1), + max_concurrent INT, + avg_concurrent DECIMAL(5,1), + total_prompt_tokens BIGINT DEFAULT 0, + total_completion_tokens BIGINT DEFAULT 0, + total_amount DECIMAL(12,4) DEFAULT 0, + userorgid VARCHAR(32), + distributor_orgid VARCHAR(32), + UNIQUE KEY uk_date_model_org (stat_date, llmid, userorgid), + INDEX idx_date (stat_date), + INDEX idx_model (model) + )""", + + """CREATE TABLE IF NOT EXISTS dm_provider_cost_daily ( + id VARCHAR(32) PRIMARY KEY, + stat_date DATE NOT NULL, + model VARCHAR(100) NOT NULL, + catelogid VARCHAR(10), + providerid VARCHAR(32) NOT NULL, + provider_name VARCHAR(100), + total_calls INT DEFAULT 0, + total_tokens BIGINT DEFAULT 0, + total_amount DECIMAL(12,4) DEFAULT 0, + unit_price DECIMAL(10,6), + avg_ttft_ms DECIMAL(10,1), + UNIQUE KEY uk_date_model_provider (stat_date, model, providerid), + INDEX idx_model (model), + INDEX idx_date (stat_date) + )""", + ] + for sql in tables: + await sor.sqlExe(sql, {}) + + +def parse_usages(usages_str): + """从 llmusage.usages JSON 中提取 token 数""" + try: + u = json.loads(usages_str) if isinstance(usages_str, str) else usages_str + if isinstance(u, dict): + return ( + u.get('prompt_tokens', 0) or 0, + u.get('completion_tokens', 0) or 0, + u.get('total_tokens', 0) or 0 + ) + except: + pass + return 0, 0, 0 + + +async def sync_call_fact(sor, last_sync=None): + """增量同步 llmusage → dm_model_call_fact""" + if last_sync is None: + last_sync = (datetime.now() - timedelta(minutes=10)).strftime('%Y-%m-%d %H:%M:%S') + + sql = """ + SELECT lu.*, l.model as llm_model, l.catelogid, l.providerid, l.ownerid + FROM llmusage lu + JOIN llm l ON l.id = lu.llmid + WHERE lu.use_time >= ${last_sync}$ + ORDER BY lu.use_time + """ + rows = await sor.sqlExe(sql, {'last_sync': last_sync}) + info('data_mart ETL: syncing ' + str(len(rows)) + ' rows from ' + last_sync) + + count = 0 + for r in rows: + r = DictObject(r) + # Skip existing + existing = await sor.sqlExe( + 'SELECT id FROM dm_model_call_fact WHERE luid=${luid}$', + {'luid': r.id}) + if existing: + continue + + prompt_tokens, completion_tokens, _ = parse_usages(r.usages) + call_time = datetime.strptime(r.use_time[:19], '%Y-%m-%d %H:%M:%S') + + ns = { + 'id': getID(), + 'luid': r.id, + 'llmid': r.llmid, + 'model': r.llm_model or r.model, + 'catelogid': r.catelogid, + 'userid': r.userid, + 'userorgid': r.userorgid, + 'ownerid': r.ownerid, + 'providerid': r.providerid, + 'call_date': r.use_date or call_time.strftime('%Y-%m-%d'), + 'call_hour': call_time.hour, + 'call_time': r.use_time, + 'ttft_ms': int(r.responsed_seconds * 1000) if r.responsed_seconds else None, + 'ttot_ms': int(r.finish_seconds * 1000) if r.finish_seconds else None, + 'prompt_tokens': prompt_tokens, + 'completion_tokens': completion_tokens, + 'status': r.status or 'SUCCEEDED', + 'amount': r.amount or 0, + 'currency': getattr(r, 'currency', 'CNY') or 'CNY', + } + await sor.C('dm_model_call_fact', ns) + count += 1 + + info('data_mart ETL: inserted ' + str(count) + ' new rows') + return count + + +async def aggregate_daily_perf(sor, stat_date=None): + """聚合当天 dm_model_call_fact → dm_model_perf_daily""" + if stat_date is None: + stat_date = datetime.now().strftime('%Y-%m-%d') + + # 按 llmid + userorgid 聚合 + sql = """ + SELECT + call_date, llmid, model, catelogid, providerid, ownerid, + COALESCE(userorgid, '') as userorgid, + COUNT(*) as total_calls, + SUM(CASE WHEN status='SUCCEEDED' THEN 1 ELSE 0 END) as success_calls, + SUM(CASE WHEN status='FAILED' THEN 1 ELSE 0 END) as fail_calls, + AVG(ttft_ms) as avg_ttft, + AVG(ttot_ms) as avg_ttot, + SUM(prompt_tokens) as total_prompt, + SUM(completion_tokens) as total_completion, + SUM(amount) as total_amount + FROM dm_model_call_fact + WHERE call_date = ${stat_date}$ + GROUP BY call_date, llmid, model, catelogid, providerid, ownerid, userorgid + """ + rows = await sor.sqlExe(sql, {'stat_date': stat_date}) + info('data_mart AGG: aggregating ' + str(len(rows)) + ' groups for ' + stat_date) + + for r in rows: + r = DictObject(r) + # Delete old row, insert new + await sor.sqlExe( + 'DELETE FROM dm_model_perf_daily WHERE stat_date=${d}$ AND llmid=${l}$ AND COALESCE(userorgid,\"\")=${u}$', + {'d': stat_date, 'l': r.llmid, 'u': r.userorgid or ''}) + + ns = { + 'id': getID(), + 'stat_date': stat_date, + 'llmid': r.llmid, + 'model': r.model, + 'catelogid': r.catelogid, + 'providerid': r.providerid, + 'ownerid': r.ownerid, + 'total_calls': r.total_calls, + 'success_calls': r.success_calls, + 'fail_calls': r.fail_calls, + 'success_rate': round(r.success_calls * 100.0 / r.total_calls, 2) if r.total_calls else 0, + 'avg_ttft_ms': round(r.avg_ttft, 1) if r.avg_ttft else None, + 'avg_ttot_ms': round(r.avg_ttot, 1) if r.avg_ttot else None, + 'total_prompt_tokens': r.total_prompt or 0, + 'total_completion_tokens': r.total_completion or 0, + 'total_amount': r.total_amount or 0, + 'userorgid': r.userorgid or None, + } + await sor.C('dm_model_perf_daily', ns) + + return len(rows) + + +async def aggregate_provider_cost(sor, stat_date=None): + """聚合同模型供应商性价比 — dm_provider_cost_daily""" + if stat_date is None: + stat_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d') + + sql = """ + SELECT + model, providerid, + COUNT(*) as total_calls, + SUM(prompt_tokens + completion_tokens) as total_tokens, + SUM(amount) as total_amount, + AVG(ttft_ms) as avg_ttft + FROM dm_model_call_fact + WHERE call_date = ${stat_date}$ + GROUP BY model, providerid + """ + rows = await sor.sqlExe(sql, {'stat_date': stat_date}) + + for r in rows: + r = DictObject(r) + await sor.sqlExe( + 'DELETE FROM dm_provider_cost_daily WHERE stat_date=${d}$ AND model=${m}$ AND providerid=${p}$', + {'d': stat_date, 'm': r.model, 'p': r.providerid}) + + ns = { + 'id': getID(), + 'stat_date': stat_date, + 'model': r.model, + 'providerid': r.providerid, + 'total_calls': r.total_calls, + 'total_tokens': r.total_tokens or 0, + 'total_amount': r.total_amount or 0, + 'unit_price': round(float(r.total_amount) / r.total_tokens, 8) if r.total_tokens else None, + 'avg_ttft_ms': round(r.avg_ttft, 1) if r.avg_ttft else None, + } + await sor.C('dm_provider_cost_daily', ns) + + return len(rows) + + +async def run_etl_sync(sor): + """每5分钟运行: 确保表存在 + 增量同步""" + await ensure_tables(sor) + await sync_call_fact(sor) + + +async def run_etl_aggregate(sor): + """每小时运行: 聚合天级性能 + 并发统计""" + await ensure_tables(sor) + await aggregate_daily_perf(sor) + + +async def run_etl_provider_cost(sor): + """每天运行: 供应商性价比""" + await ensure_tables(sor) + await aggregate_provider_cost(sor) diff --git a/data_mart/init.py b/data_mart/init.py new file mode 100644 index 0000000..828b89a --- /dev/null +++ b/data_mart/init.py @@ -0,0 +1,78 @@ +"""data_mart 模块初始化""" +from ahserver.serverenv import ServerEnv +from appPublic.jsonConfig import getConfig +from appPublic.log import debug, exception +from sqlor.dbpools import DBPools +from .etl import sync_call_fact, aggregate_daily_perf, aggregate_provider_cost, ensure_tables + +MODULE_NAME = "data_mart" + + +def get_dbname(): + env = ServerEnv() + return env.get_module_dbname(MODULE_NAME) + + +async def api_model_perf(sor, params_kw=None): + """模型性能统计 API — POST /data_mart/api/model_perf.dspy""" + model = params_kw.get('model') if params_kw else None + stat_date = params_kw.get('date') if params_kw else None + + sql = "SELECT * FROM dm_model_perf_daily WHERE 1=1" + ns = {} + if stat_date: + sql += " AND stat_date=${stat_date}$" + ns['stat_date'] = stat_date + if model: + sql += " AND model=${model}$" + ns['model'] = model + sql += " ORDER BY total_calls DESC LIMIT 50" + + rows = await sor.sqlExe(sql, ns) + return [dict(r) for r in rows] + + +async def api_provider_roi(sor, params_kw=None): + """供应商性价比 API — POST /data_mart/api/provider_roi.dspy""" + model = params_kw.get('model') if params_kw else None + stat_date = params_kw.get('date') if params_kw else None + + sql = "SELECT * FROM dm_provider_cost_daily WHERE 1=1" + ns = {} + if stat_date: + sql += " AND stat_date=${stat_date}$" + ns['stat_date'] = stat_date + if model: + sql += " AND model=${model}$" + ns['model'] = model + sql += " ORDER BY total_amount DESC LIMIT 50" + + rows = await sor.sqlExe(sql, ns) + return [dict(r) for r in rows] + + +async def api_call_detail(sor, params_kw=None): + """调用明细 API — POST /data_mart/api/call_detail.dspy""" + stat_date = (params_kw or {}).get('date') + userorgid = (params_kw or {}).get('userorgid') + + sql = "SELECT * FROM dm_model_call_fact WHERE 1=1" + ns = {} + if stat_date: + sql += " AND call_date=${stat_date}$" + ns['stat_date'] = stat_date + if userorgid: + sql += " AND userorgid=${userorgid}$" + ns['userorgid'] = userorgid + sql += " ORDER BY call_time DESC LIMIT 200" + + rows = await sor.sqlExe(sql, ns) + return [dict(r) for r in rows] + + +def load_data_mart(): + env = ServerEnv() + env.api_model_perf = api_model_perf + env.api_provider_roi = api_provider_roi + env.api_call_detail = api_call_detail + debug("[data_mart] registered API endpoints") diff --git a/i18n/zh/msg.txt b/i18n/zh/msg.txt new file mode 100644 index 0000000..ebf01cf --- /dev/null +++ b/i18n/zh/msg.txt @@ -0,0 +1 @@ +{"模型性能监控":"Model Performance","供应商性价比对比":"Provider ROI","平均 TTFT":"Avg TTFT","成功率":"Success Rate","今日调用":"Today Calls","失败数":"Failures","模型性能":"Model Perf","供应商性价比":"Provider ROI","刷新":"Refresh"} \ No newline at end of file diff --git a/json/build.sh b/json/build.sh new file mode 100644 index 0000000..18bcd7a --- /dev/null +++ b/json/build.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# data_mart has no CRUD tables - skip xls2ui +echo "[data_mart] json build skipped (no CRUD)" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3c801b5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,12 @@ +[build-system] +requires = ["setuptools>=61", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "data_mart" +version = "0.1.0" +description = "Sage 数据集市 — 模型性能指标、供应商性价比、多维度分析" +requires-python = ">=3.10" + +[tool.setuptools.packages.find] +include = ["data_mart*"] diff --git a/scripts/load_path.py b/scripts/load_path.py new file mode 100644 index 0000000..c7ec806 --- /dev/null +++ b/scripts/load_path.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""data_mart 模块 load_path""" +import os, sys, subprocess + +MOD = "data_mart" +SAGE_ROOT = os.environ.get("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") + +PATHS_LOGINED = [ + f"/{MOD}", + f"/{MOD}/index.ui", + f"/{MOD}/provider_roi.ui", + f"/{MOD}/stat_avg_ttft.ui", + f"/{MOD}/stat_success_rate.ui", + f"/{MOD}/stat_total_calls.ui", + f"/{MOD}/stat_fail_calls.ui", + f"/{MOD}/api/%", +] + +def run(role, path): + subprocess.run([PYTHON, SET_PERM, role, path], capture_output=True) + +for p in PATHS_LOGINED: + run("logined", p) + +print(f"[{MOD}] {len(PATHS_LOGINED)} paths registered") diff --git a/wwwroot/api/model_perf.dspy b/wwwroot/api/model_perf.dspy new file mode 100644 index 0000000..42a4226 --- /dev/null +++ b/wwwroot/api/model_perf.dspy @@ -0,0 +1,5 @@ +# 模型性能统计 API +env = request._run_ns +async with get_sor_context(env, 'data_mart') as sor: + rows = await env.api_model_perf(sor, params_kw) + return json.dumps(rows, ensure_ascii=False) diff --git a/wwwroot/api/provider_roi.dspy b/wwwroot/api/provider_roi.dspy new file mode 100644 index 0000000..34998dc --- /dev/null +++ b/wwwroot/api/provider_roi.dspy @@ -0,0 +1,5 @@ +# 供应商性价比 API +env = request._run_ns +async with get_sor_context(env, 'data_mart') as sor: + rows = await env.api_provider_roi(sor, params_kw) + return json.dumps(rows, ensure_ascii=False) diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..0b62b6a --- /dev/null +++ b/wwwroot/index.ui @@ -0,0 +1,58 @@ +{ + "widgettype": "VBox", + "id": "datamart_root", + "options": {"width": "100%", "height": "100%", "bgcolor": "var(--sage-bg-primary, transparent)"}, + "subwidgets": [ + { + "widgettype": "VScrollPanel", + "options": {"css": "filler"}, + "subwidgets": [ + { + "widgettype": "HBox", + "options": {"width": "100%", "alignItems": "center", "marginBottom": "12px"}, + "subwidgets": [ + {"widgettype": "Title2", "options": {"fontWeight": "700", "otext": "模型性能监控", "i18n": true}}, + {"widgettype": "Filler"}, + { + "widgettype": "Button", + "options": {"label": "刷新", "fontSize": "12px"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "method", "target": "-@Cols", "method": "render_urldata", "params": {}}] + } + ] + }, + { + "widgettype": "HBox", + "options": {"width": "100%", "gap": "16px", "marginBottom": "16px"}, + "subwidgets": [ + {"widgettype": "RefreshWidget", "id": "stat_avg_ttft", "options": {"period_seconds": 60, "url": "{{entire_url('stat_avg_ttft.ui')}}", "width": "25%"}}, + {"widgettype": "RefreshWidget", "id": "stat_success_rate", "options": {"period_seconds": 60, "url": "{{entire_url('stat_success_rate.ui')}}", "width": "25%"}}, + {"widgettype": "RefreshWidget", "id": "stat_total_calls", "options": {"period_seconds": 60, "url": "{{entire_url('stat_total_calls.ui')}}", "width": "25%"}}, + {"widgettype": "RefreshWidget", "id": "stat_fail_calls", "options": {"period_seconds": 60, "url": "{{entire_url('stat_fail_calls.ui')}}", "width": "25%"}} + ] + }, + { + "widgettype": "Cols", + "options": { + "css": "card", + "col_cwidth": 28, + "title": "模型性能排行", + "data_url": "{{entire_url('api/model_perf.dspy')}}", + "data_params": {}, + "record_view": { + "widgettype": "HBox", + "options": {"cheight": 4, "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "${model}", "width": "30%"}}, + {"widgettype": "Text", "options": {"text": "${avg_ttft_ms}ms", "width": "15%"}}, + {"widgettype": "Text", "options": {"text": "${avg_ttot_ms}ms", "width": "15%"}}, + {"widgettype": "Text", "options": {"text": "${success_rate}%", "width": "15%"}}, + {"widgettype": "Text", "options": {"text": "${total_calls}", "width": "15%"}}, + {"widgettype": "Text", "options": {"text": "¥${total_amount}", "width": "10%"}} + ] + } + } + } + ] + } + ] +} diff --git a/wwwroot/menu.ui b/wwwroot/menu.ui new file mode 100644 index 0000000..ff1e38c --- /dev/null +++ b/wwwroot/menu.ui @@ -0,0 +1,22 @@ +{ + "widgettype": "VBox", + "options": {"width": "100%"}, + "subwidgets": [ + { + "widgettype": "MenuItem", + "options": { + "label": "模型性能", + "url": "{{entire_url('/data_mart/index.ui')}}", + "icon": "{{entire_url('/data_mart/imgs/perf.svg')}}" + } + }, + { + "widgettype": "MenuItem", + "options": { + "label": "供应商性价比", + "url": "{{entire_url('/data_mart/provider_roi.ui')}}", + "icon": "{{entire_url('/data_mart/imgs/roi.svg')}}" + } + } + ] +} diff --git a/wwwroot/provider_roi.ui b/wwwroot/provider_roi.ui new file mode 100644 index 0000000..323531b --- /dev/null +++ b/wwwroot/provider_roi.ui @@ -0,0 +1,48 @@ +{ + "widgettype": "VBox", + "id": "provider_roi_root", + "options": {"width": "100%", "height": "100%"}, + "subwidgets": [ + { + "widgettype": "VScrollPanel", + "options": {"css": "filler"}, + "subwidgets": [ + { + "widgettype": "HBox", + "options": {"width": "100%", "alignItems": "center", "marginBottom": "12px"}, + "subwidgets": [ + {"widgettype": "Title2", "options": {"fontWeight": "700", "otext": "供应商性价比对比", "i18n": true}}, + {"widgettype": "Filler"}, + { + "widgettype": "Button", + "options": {"label": "刷新", "fontSize": "12px"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "method", "target": "-@Cols", "method": "render_urldata", "params": {}}] + } + ] + }, + { + "widgettype": "Cols", + "options": { + "css": "card", + "col_cwidth": 28, + "title": "同模型供应商价格对比", + "data_url": "{{entire_url('api/provider_roi.dspy')}}", + "data_params": {}, + "record_view": { + "widgettype": "HBox", + "options": {"cheight": 4, "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "${model}", "width": "30%"}}, + {"widgettype": "Text", "options": {"text": "${providerid}", "width": "20%"}}, + {"widgettype": "Text", "options": {"text": "${total_calls}次", "width": "12%"}}, + {"widgettype": "Text", "options": {"text": "¥${unit_price}/t", "width": "15%"}}, + {"widgettype": "Text", "options": {"text": "${avg_ttft_ms}ms", "width": "13%"}}, + {"widgettype": "Text", "options": {"text": "¥${total_amount}", "width": "10%"}} + ] + } + } + } + ] + } + ] +} diff --git a/wwwroot/stat_avg_ttft.ui b/wwwroot/stat_avg_ttft.ui new file mode 100644 index 0000000..84e55dd --- /dev/null +++ b/wwwroot/stat_avg_ttft.ui @@ -0,0 +1,12 @@ +{% set rows = api_model_perf(request) %} +{% set total = [] %}{% set avg = 0 %}{% set cnt = 0 %} +{% for r in rows %}{% if r.avg_ttft_ms %}{% set avg = avg + r.avg_ttft_ms %}{% set cnt = cnt + 1 %}{% endif %}{% endfor %} +{% if cnt > 0 %}{% set avg = (avg / cnt)|round(0) %}{% endif %} +{ + "widgettype": "VBox", + "options": {"css": "card", "padding": "16px", "borderRadius": "12px", "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "平均 TTFT", "fontSize": "12px", "color": "var(--sage-text-secondary)"}}, + {"widgettype": "Title2", "options": {"text": "{{avg}}ms", "fontWeight": "700", "marginTop": "4px"}} + ] +} diff --git a/wwwroot/stat_fail_calls.ui b/wwwroot/stat_fail_calls.ui new file mode 100644 index 0000000..c1a6366 --- /dev/null +++ b/wwwroot/stat_fail_calls.ui @@ -0,0 +1,2 @@ +{% set rows = api_model_perf(request) %}{% set t = 0 %}{% for r in rows %}{% set t = t + r.fail_calls %}{% endfor %} +{"widgettype":"VBox","options":{"css":"card","padding":"16px","borderRadius":"12px","width":"100%"},"subwidgets":[{"widgettype":"Text","options":{"text":"失败数","fontSize":"12px","color":"var(--sage-text-secondary)"}},{"widgettype":"Title2","options":{"text":"{{t}}","fontWeight":"700","marginTop":"4px"}}]} diff --git a/wwwroot/stat_success_rate.ui b/wwwroot/stat_success_rate.ui new file mode 100644 index 0000000..daaad73 --- /dev/null +++ b/wwwroot/stat_success_rate.ui @@ -0,0 +1,12 @@ +{% set rows = api_model_perf(request) %} +{% set total = 0 %}{% set succ = 0 %} +{% for r in rows %}{% set total = total + r.total_calls %}{% set succ = succ + r.success_calls %}{% endfor %} +{% set rate = (succ * 100 / total)|round(1) if total > 0 else 0 %} +{ + "widgettype": "VBox", + "options": {"css": "card", "padding": "16px", "borderRadius": "12px", "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "成功率", "fontSize": "12px", "color": "var(--sage-text-secondary)"}}, + {"widgettype": "Title2", "options": {"text": "{{rate}}%", "fontWeight": "700", "marginTop": "4px"}} + ] +} diff --git a/wwwroot/stat_total_calls.ui b/wwwroot/stat_total_calls.ui new file mode 100644 index 0000000..d88703d --- /dev/null +++ b/wwwroot/stat_total_calls.ui @@ -0,0 +1,2 @@ +{% set rows = api_model_perf(request) %}{% set t = 0 %}{% for r in rows %}{% set t = t + r.total_calls %}{% endfor %} +{"widgettype":"VBox","options":{"css":"card","padding":"16px","borderRadius":"12px","width":"100%"},"subwidgets":[{"widgettype":"Text","options":{"text":"今日调用","fontSize":"12px","color":"var(--sage-text-secondary)"}},{"widgettype":"Title2","options":{"text":"{{t}}","fontWeight":"700","marginTop":"4px"}}]}