feat: enhance MainLayout with brand name expansion feature, sidebar toggle improvements, and responsive design adjustments for better user experience

This commit is contained in:
Supra4E8C
2025-12-09 19:04:40 +08:00
parent 5d5334afb1
commit 18b1adb4e2
7 changed files with 1896 additions and 187 deletions

View File

@@ -15,6 +15,50 @@ export interface KeyStats {
byAuthIndex: Record<string, KeyStatBucket>;
}
export interface TokenBreakdown {
cachedTokens: number;
reasoningTokens: number;
}
export interface RateStats {
rpm: number;
tpm: number;
windowMinutes: number;
requestCount: number;
tokenCount: number;
}
export interface ModelPrice {
prompt: number;
completion: number;
}
export interface UsageDetail {
timestamp: string;
source: string;
auth_index: number;
tokens: {
input_tokens: number;
output_tokens: number;
reasoning_tokens: number;
cached_tokens: number;
total_tokens: number;
};
failed: boolean;
__modelName?: string;
}
export interface ApiStats {
endpoint: string;
totalRequests: number;
totalTokens: number;
totalCost: number;
models: Record<string, { requests: number; tokens: number }>;
}
const TOKENS_PER_PRICE_UNIT = 1_000_000;
const MODEL_PRICE_STORAGE_KEY = 'cli-proxy-model-prices-v2';
const normalizeAuthIndex = (value: any) => {
if (typeof value === 'number' && Number.isFinite(value)) {
return value.toString();
@@ -70,6 +114,580 @@ export function maskUsageSensitiveValue(value: unknown, masker: (val: string) =>
return masked;
}
/**
* 格式化 tokens 为百万单位
*/
export function formatTokensInMillions(value: number): string {
const num = Number(value);
if (!Number.isFinite(num)) {
return '0.00M';
}
return `${(num / 1_000_000).toFixed(2)}M`;
}
/**
* 格式化每分钟数值
*/
export function formatPerMinuteValue(value: number): string {
const num = Number(value);
if (!Number.isFinite(num)) {
return '0.00';
}
const abs = Math.abs(num);
if (abs >= 1000) {
return Math.round(num).toLocaleString();
}
if (abs >= 100) {
return num.toFixed(0);
}
if (abs >= 10) {
return num.toFixed(1);
}
return num.toFixed(2);
}
/**
* 格式化紧凑数字
*/
export function formatCompactNumber(value: number): string {
const num = Number(value);
if (!Number.isFinite(num)) {
return '0';
}
const abs = Math.abs(num);
if (abs >= 1_000_000) {
return `${(num / 1_000_000).toFixed(1)}M`;
}
if (abs >= 1_000) {
return `${(num / 1_000).toFixed(1)}K`;
}
return abs >= 1 ? num.toFixed(0) : num.toFixed(2);
}
/**
* 格式化美元
*/
export function formatUsd(value: number): string {
const num = Number(value);
if (!Number.isFinite(num)) {
return '$0.00';
}
const fixed = num.toFixed(2);
const parts = Number(fixed).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
return `$${parts}`;
}
/**
* 从使用数据中收集所有请求明细
*/
export function collectUsageDetails(usageData: any): UsageDetail[] {
if (!usageData) {
return [];
}
const apis = usageData.apis || {};
const details: UsageDetail[] = [];
Object.values(apis as Record<string, any>).forEach((apiEntry) => {
const models = apiEntry?.models || {};
Object.entries(models as Record<string, any>).forEach(([modelName, modelEntry]) => {
const modelDetails = Array.isArray(modelEntry.details) ? modelEntry.details : [];
modelDetails.forEach((detail: any) => {
if (detail && detail.timestamp) {
details.push({
...detail,
__modelName: modelName
});
}
});
});
});
return details;
}
/**
* 从单条明细提取总 tokens
*/
export function extractTotalTokens(detail: any): number {
const tokens = detail?.tokens || {};
if (typeof tokens.total_tokens === 'number') {
return tokens.total_tokens;
}
const tokenKeys = ['input_tokens', 'output_tokens', 'reasoning_tokens', 'cached_tokens'];
return tokenKeys.reduce((sum, key) => {
const value = tokens[key];
return sum + (typeof value === 'number' ? value : 0);
}, 0);
}
/**
* 计算 token 分类统计
*/
export function calculateTokenBreakdown(usageData: any): TokenBreakdown {
const details = collectUsageDetails(usageData);
if (!details.length) {
return { cachedTokens: 0, reasoningTokens: 0 };
}
let cachedTokens = 0;
let reasoningTokens = 0;
details.forEach(detail => {
const tokens = detail?.tokens || {};
if (typeof tokens.cached_tokens === 'number') {
cachedTokens += tokens.cached_tokens;
}
if (typeof tokens.reasoning_tokens === 'number') {
reasoningTokens += tokens.reasoning_tokens;
}
});
return { cachedTokens, reasoningTokens };
}
/**
* 计算最近 N 分钟的 RPM/TPM
*/
export function calculateRecentPerMinuteRates(windowMinutes: number = 30, usageData: any): RateStats {
const details = collectUsageDetails(usageData);
const effectiveWindow = Number.isFinite(windowMinutes) && windowMinutes > 0 ? windowMinutes : 30;
if (!details.length) {
return { rpm: 0, tpm: 0, windowMinutes: effectiveWindow, requestCount: 0, tokenCount: 0 };
}
const now = Date.now();
const windowStart = now - effectiveWindow * 60 * 1000;
let requestCount = 0;
let tokenCount = 0;
details.forEach(detail => {
const timestamp = Date.parse(detail.timestamp);
if (Number.isNaN(timestamp) || timestamp < windowStart) {
return;
}
requestCount += 1;
tokenCount += extractTotalTokens(detail);
});
const denominator = effectiveWindow > 0 ? effectiveWindow : 1;
return {
rpm: requestCount / denominator,
tpm: tokenCount / denominator,
windowMinutes: effectiveWindow,
requestCount,
tokenCount
};
}
/**
* 从使用数据获取模型名称列表
*/
export function getModelNamesFromUsage(usageData: any): string[] {
if (!usageData) {
return [];
}
const apis = usageData.apis || {};
const names = new Set<string>();
Object.values(apis as Record<string, any>).forEach(apiEntry => {
const models = apiEntry?.models || {};
Object.keys(models).forEach(modelName => {
if (modelName) {
names.add(modelName);
}
});
});
return Array.from(names).sort((a, b) => a.localeCompare(b));
}
/**
* 计算成本数据
*/
export function calculateCost(detail: any, modelPrices: Record<string, ModelPrice>): number {
const modelName = detail.__modelName || '';
const price = modelPrices[modelName];
if (!price) {
return 0;
}
const tokens = detail?.tokens || {};
const promptTokens = Number(tokens.input_tokens) || 0;
const completionTokens = Number(tokens.output_tokens) || 0;
const promptCost = (promptTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.prompt) || 0);
const completionCost = (completionTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.completion) || 0);
const total = promptCost + completionCost;
return Number.isFinite(total) && total > 0 ? total : 0;
}
/**
* 计算总成本
*/
export function calculateTotalCost(usageData: any, modelPrices: Record<string, ModelPrice>): number {
const details = collectUsageDetails(usageData);
if (!details.length || !Object.keys(modelPrices).length) {
return 0;
}
return details.reduce((sum, detail) => sum + calculateCost(detail, modelPrices), 0);
}
/**
* 从 localStorage 加载模型价格
*/
export function loadModelPrices(): Record<string, ModelPrice> {
try {
if (typeof localStorage === 'undefined') {
return {};
}
const raw = localStorage.getItem(MODEL_PRICE_STORAGE_KEY);
if (!raw) {
return {};
}
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') {
return {};
}
const normalized: Record<string, ModelPrice> = {};
Object.entries(parsed).forEach(([model, price]: [string, any]) => {
if (!model) return;
const prompt = Number(price?.prompt);
const completion = Number(price?.completion);
if (!Number.isFinite(prompt) && !Number.isFinite(completion)) {
return;
}
normalized[model] = {
prompt: Number.isFinite(prompt) && prompt >= 0 ? prompt : 0,
completion: Number.isFinite(completion) && completion >= 0 ? completion : 0
};
});
return normalized;
} catch {
return {};
}
}
/**
* 保存模型价格到 localStorage
*/
export function saveModelPrices(prices: Record<string, ModelPrice>): void {
try {
if (typeof localStorage === 'undefined') {
return;
}
localStorage.setItem(MODEL_PRICE_STORAGE_KEY, JSON.stringify(prices));
} catch {
console.warn('保存模型价格失败');
}
}
/**
* 获取 API 统计数据
*/
export function getApiStats(usageData: any, modelPrices: Record<string, ModelPrice>): ApiStats[] {
if (!usageData?.apis) {
return [];
}
const apis = usageData.apis;
const result: ApiStats[] = [];
Object.entries(apis as Record<string, any>).forEach(([endpoint, apiData]) => {
const models: Record<string, { requests: number; tokens: number }> = {};
let totalCost = 0;
const modelsData = apiData?.models || {};
Object.entries(modelsData as Record<string, any>).forEach(([modelName, modelData]) => {
models[modelName] = {
requests: modelData.total_requests || 0,
tokens: modelData.total_tokens || 0
};
const price = modelPrices[modelName];
if (price) {
const details = Array.isArray(modelData.details) ? modelData.details : [];
details.forEach((detail: any) => {
const tokens = detail?.tokens || {};
const promptTokens = Number(tokens.input_tokens) || 0;
const completionTokens = Number(tokens.output_tokens) || 0;
const cost = (promptTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.prompt) || 0) +
(completionTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.completion) || 0);
if (Number.isFinite(cost) && cost > 0) {
totalCost += cost;
}
});
}
});
result.push({
endpoint: maskUsageSensitiveValue(endpoint) || endpoint,
totalRequests: apiData.total_requests || 0,
totalTokens: apiData.total_tokens || 0,
totalCost,
models
});
});
return result;
}
/**
* 获取模型统计数据
*/
export function getModelStats(usageData: any, modelPrices: Record<string, ModelPrice>): Array<{
model: string;
requests: number;
tokens: number;
cost: number;
}> {
if (!usageData?.apis) {
return [];
}
const modelMap = new Map<string, { requests: number; tokens: number; cost: number }>();
Object.values(usageData.apis as Record<string, any>).forEach(apiData => {
const models = apiData?.models || {};
Object.entries(models as Record<string, any>).forEach(([modelName, modelData]) => {
const existing = modelMap.get(modelName) || { requests: 0, tokens: 0, cost: 0 };
existing.requests += modelData.total_requests || 0;
existing.tokens += modelData.total_tokens || 0;
const price = modelPrices[modelName];
if (price) {
const details = Array.isArray(modelData.details) ? modelData.details : [];
details.forEach((detail: any) => {
const tokens = detail?.tokens || {};
const promptTokens = Number(tokens.input_tokens) || 0;
const completionTokens = Number(tokens.output_tokens) || 0;
const cost = (promptTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.prompt) || 0) +
(completionTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.completion) || 0);
if (Number.isFinite(cost) && cost > 0) {
existing.cost += cost;
}
});
}
modelMap.set(modelName, existing);
});
});
return Array.from(modelMap.entries())
.map(([model, stats]) => ({ model, ...stats }))
.sort((a, b) => b.requests - a.requests);
}
/**
* 格式化小时标签
*/
export function formatHourLabel(date: Date): string {
if (!(date instanceof Date)) {
return '';
}
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hour = date.getHours().toString().padStart(2, '0');
return `${month}-${day} ${hour}:00`;
}
/**
* 格式化日期标签
*/
export function formatDayLabel(date: Date): string {
if (!(date instanceof Date)) {
return '';
}
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* 构建小时级别的数据序列
*/
export function buildHourlySeriesByModel(usageData: any, metric: 'requests' | 'tokens' = 'requests'): {
labels: string[];
dataByModel: Map<string, number[]>;
hasData: boolean;
} {
const hourMs = 60 * 60 * 1000;
const now = new Date();
const currentHour = new Date(now);
currentHour.setMinutes(0, 0, 0);
const earliestBucket = new Date(currentHour);
earliestBucket.setHours(earliestBucket.getHours() - 23);
const earliestTime = earliestBucket.getTime();
const labels: string[] = [];
for (let i = 0; i < 24; i++) {
const bucketStart = earliestTime + i * hourMs;
labels.push(formatHourLabel(new Date(bucketStart)));
}
const details = collectUsageDetails(usageData);
const dataByModel = new Map<string, number[]>();
let hasData = false;
if (!details.length) {
return { labels, dataByModel, hasData };
}
details.forEach(detail => {
const timestamp = Date.parse(detail.timestamp);
if (Number.isNaN(timestamp)) {
return;
}
const normalized = new Date(timestamp);
normalized.setMinutes(0, 0, 0);
const bucketStart = normalized.getTime();
const lastBucketTime = earliestTime + (labels.length - 1) * hourMs;
if (bucketStart < earliestTime || bucketStart > lastBucketTime) {
return;
}
const bucketIndex = Math.floor((bucketStart - earliestTime) / hourMs);
if (bucketIndex < 0 || bucketIndex >= labels.length) {
return;
}
const modelName = detail.__modelName || 'Unknown';
if (!dataByModel.has(modelName)) {
dataByModel.set(modelName, new Array(labels.length).fill(0));
}
const bucketValues = dataByModel.get(modelName)!;
if (metric === 'tokens') {
bucketValues[bucketIndex] += extractTotalTokens(detail);
} else {
bucketValues[bucketIndex] += 1;
}
hasData = true;
});
return { labels, dataByModel, hasData };
}
/**
* 构建日级别的数据序列
*/
export function buildDailySeriesByModel(usageData: any, metric: 'requests' | 'tokens' = 'requests'): {
labels: string[];
dataByModel: Map<string, number[]>;
hasData: boolean;
} {
const details = collectUsageDetails(usageData);
const valuesByModel = new Map<string, Map<string, number>>();
const labelsSet = new Set<string>();
let hasData = false;
if (!details.length) {
return { labels: [], dataByModel: new Map(), hasData };
}
details.forEach(detail => {
const timestamp = Date.parse(detail.timestamp);
if (Number.isNaN(timestamp)) {
return;
}
const dayLabel = formatDayLabel(new Date(timestamp));
if (!dayLabel) {
return;
}
const modelName = detail.__modelName || 'Unknown';
if (!valuesByModel.has(modelName)) {
valuesByModel.set(modelName, new Map());
}
const modelDayMap = valuesByModel.get(modelName)!;
const increment = metric === 'tokens' ? extractTotalTokens(detail) : 1;
modelDayMap.set(dayLabel, (modelDayMap.get(dayLabel) || 0) + increment);
labelsSet.add(dayLabel);
hasData = true;
});
const labels = Array.from(labelsSet).sort();
const dataByModel = new Map<string, number[]>();
valuesByModel.forEach((dayMap, modelName) => {
const series = labels.map(label => dayMap.get(label) || 0);
dataByModel.set(modelName, series);
});
return { labels, dataByModel, hasData };
}
export interface ChartDataset {
label: string;
data: number[];
borderColor: string;
backgroundColor: string;
fill: boolean;
tension: number;
}
export interface ChartData {
labels: string[];
datasets: ChartDataset[];
}
const CHART_COLORS = [
{ borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.15)' },
{ borderColor: '#22c55e', backgroundColor: 'rgba(34, 197, 94, 0.15)' },
{ borderColor: '#f59e0b', backgroundColor: 'rgba(245, 158, 11, 0.15)' },
{ borderColor: '#ef4444', backgroundColor: 'rgba(239, 68, 68, 0.15)' },
{ borderColor: '#8b5cf6', backgroundColor: 'rgba(139, 92, 246, 0.15)' },
{ borderColor: '#06b6d4', backgroundColor: 'rgba(6, 182, 212, 0.15)' },
{ borderColor: '#ec4899', backgroundColor: 'rgba(236, 72, 153, 0.15)' },
{ borderColor: '#84cc16', backgroundColor: 'rgba(132, 204, 22, 0.15)' },
{ borderColor: '#f97316', backgroundColor: 'rgba(249, 115, 22, 0.15)' },
];
/**
* 构建图表数据
*/
export function buildChartData(
usageData: any,
period: 'hour' | 'day' = 'day',
metric: 'requests' | 'tokens' = 'requests',
selectedModels: string[] = []
): ChartData {
const baseSeries = period === 'hour'
? buildHourlySeriesByModel(usageData, metric)
: buildDailySeriesByModel(usageData, metric);
const { labels, dataByModel } = baseSeries;
// Build "All" series as sum of all models
const getAllSeries = (): number[] => {
const summed = new Array(labels.length).fill(0);
dataByModel.forEach(values => {
values.forEach((value, idx) => {
summed[idx] = (summed[idx] || 0) + value;
});
});
return summed;
};
// Determine which models to show
const modelsToShow = selectedModels.length > 0 ? selectedModels : ['all'];
const datasets: ChartDataset[] = modelsToShow.map((model, index) => {
const isAll = model === 'all';
const data = isAll ? getAllSeries() : (dataByModel.get(model) || new Array(labels.length).fill(0));
const colorIndex = index % CHART_COLORS.length;
const style = CHART_COLORS[colorIndex];
return {
label: isAll ? 'All Models' : model,
data,
borderColor: style.borderColor,
backgroundColor: style.backgroundColor,
fill: false,
tension: 0.35
};
});
return { labels, datasets };
}
/**
* 依据 usage 数据计算密钥使用统计
*/