mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 优化仪表盘数据展示和用户体验
- 优化实时TPM显示格式,添加智能单位(K/M) - 折线图tooltip添加token数单位格式化 - API Keys使用趋势图增加USD金额计算和显示 - 实现鼠标悬浮时数据倒序展示,前3名添加🥇🥈🥉标识 - 后端API添加费用计算支持 - 删除测试脚本,保持代码整洁 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,69 +0,0 @@
|
||||
const redis = require('../src/models/redis');
|
||||
const logger = require('../src/utils/logger');
|
||||
|
||||
async function testRealtimeMetrics() {
|
||||
try {
|
||||
// 连接Redis
|
||||
await redis.connect();
|
||||
|
||||
// 获取当前时间戳
|
||||
const now = new Date();
|
||||
const currentMinute = Math.floor(now.getTime() / 60000);
|
||||
|
||||
console.log('=== 时间戳测试 ===');
|
||||
console.log('当前时间:', now.toISOString());
|
||||
console.log('当前分钟时间戳:', currentMinute);
|
||||
console.log('');
|
||||
|
||||
// 检查最近5分钟的键
|
||||
console.log('=== 检查Redis键 ===');
|
||||
const client = redis.getClient();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const minuteKey = `system:metrics:minute:${currentMinute - i}`;
|
||||
const exists = await client.exists(minuteKey);
|
||||
const data = await client.hgetall(minuteKey);
|
||||
|
||||
console.log(`键: ${minuteKey}`);
|
||||
console.log(` 存在: ${exists ? '是' : '否'}`);
|
||||
if (exists && data) {
|
||||
console.log(` 数据: requests=${data.requests}, totalTokens=${data.totalTokens}`);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// 调用getRealtimeSystemMetrics
|
||||
console.log('=== 调用 getRealtimeSystemMetrics ===');
|
||||
const metrics = await redis.getRealtimeSystemMetrics();
|
||||
console.log('结果:', JSON.stringify(metrics, null, 2));
|
||||
|
||||
// 列出所有system:metrics:minute:*键
|
||||
console.log('\n=== 所有系统指标键 ===');
|
||||
const allKeys = await client.keys('system:metrics:minute:*');
|
||||
console.log('找到的键数量:', allKeys.length);
|
||||
if (allKeys.length > 0) {
|
||||
// 排序并显示最新的10个
|
||||
allKeys.sort((a, b) => {
|
||||
const aNum = parseInt(a.split(':').pop());
|
||||
const bNum = parseInt(b.split(':').pop());
|
||||
return bNum - aNum;
|
||||
});
|
||||
|
||||
console.log('最新的10个键:');
|
||||
for (let i = 0; i < Math.min(10, allKeys.length); i++) {
|
||||
const key = allKeys[i];
|
||||
const timestamp = parseInt(key.split(':').pop());
|
||||
const timeDiff = currentMinute - timestamp;
|
||||
console.log(` ${key} (${timeDiff}分钟前)`);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('测试失败:', error);
|
||||
} finally {
|
||||
await redis.disconnect();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
testRealtimeMetrics();
|
||||
@@ -1073,7 +1073,7 @@ class RedisClient {
|
||||
totalCacheReadTokens
|
||||
};
|
||||
|
||||
logger.debug(`🔍 Realtime metrics - Final result:`, result);
|
||||
logger.debug('🔍 Realtime metrics - Final result:', result);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1986,15 +1986,27 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
const data = await client.hgetall(key);
|
||||
|
||||
if (data && apiKeyMap.has(apiKeyId)) {
|
||||
const totalTokens = (parseInt(data.inputTokens) || 0) +
|
||||
(parseInt(data.outputTokens) || 0) +
|
||||
(parseInt(data.cacheCreateTokens) || 0) +
|
||||
(parseInt(data.cacheReadTokens) || 0);
|
||||
const inputTokens = parseInt(data.inputTokens) || 0;
|
||||
const outputTokens = parseInt(data.outputTokens) || 0;
|
||||
const cacheCreateTokens = parseInt(data.cacheCreateTokens) || 0;
|
||||
const cacheReadTokens = parseInt(data.cacheReadTokens) || 0;
|
||||
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
|
||||
|
||||
// 计算费用 - 使用默认模型价格,因为小时级别的数据没有模型信息
|
||||
const usage = {
|
||||
input_tokens: inputTokens,
|
||||
output_tokens: outputTokens,
|
||||
cache_creation_input_tokens: cacheCreateTokens,
|
||||
cache_read_input_tokens: cacheReadTokens
|
||||
};
|
||||
const costResult = CostCalculator.calculateCost(usage, 'claude-3-5-haiku-20241022');
|
||||
|
||||
hourData.apiKeys[apiKeyId] = {
|
||||
name: apiKeyMap.get(apiKeyId).name,
|
||||
tokens: totalTokens,
|
||||
requests: parseInt(data.requests) || 0
|
||||
requests: parseInt(data.requests) || 0,
|
||||
cost: costResult.costs.total,
|
||||
formattedCost: costResult.formatted.total
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2031,15 +2043,27 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
const data = await client.hgetall(key);
|
||||
|
||||
if (data && apiKeyMap.has(apiKeyId)) {
|
||||
const totalTokens = (parseInt(data.inputTokens) || 0) +
|
||||
(parseInt(data.outputTokens) || 0) +
|
||||
(parseInt(data.cacheCreateTokens) || 0) +
|
||||
(parseInt(data.cacheReadTokens) || 0);
|
||||
const inputTokens = parseInt(data.inputTokens) || 0;
|
||||
const outputTokens = parseInt(data.outputTokens) || 0;
|
||||
const cacheCreateTokens = parseInt(data.cacheCreateTokens) || 0;
|
||||
const cacheReadTokens = parseInt(data.cacheReadTokens) || 0;
|
||||
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
|
||||
|
||||
// 计算费用 - 使用默认模型价格,因为日级别的汇总数据没有模型信息
|
||||
const usage = {
|
||||
input_tokens: inputTokens,
|
||||
output_tokens: outputTokens,
|
||||
cache_creation_input_tokens: cacheCreateTokens,
|
||||
cache_read_input_tokens: cacheReadTokens
|
||||
};
|
||||
const costResult = CostCalculator.calculateCost(usage, 'claude-3-5-haiku-20241022');
|
||||
|
||||
dayData.apiKeys[apiKeyId] = {
|
||||
name: apiKeyMap.get(apiKeyId).name,
|
||||
tokens: totalTokens,
|
||||
requests: parseInt(data.requests) || 0
|
||||
requests: parseInt(data.requests) || 0,
|
||||
cost: costResult.costs.total,
|
||||
formattedCost: costResult.formatted.total
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
实时TPM
|
||||
<span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}分钟)</span>
|
||||
</p>
|
||||
<p class="text-3xl font-bold text-rose-600">{{ dashboardData.realtimeTPM || 0 }}</p>
|
||||
<p class="text-3xl font-bold text-rose-600">{{ formatNumber(dashboardData.realtimeTPM || 0) }}</p>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
每分钟Token数
|
||||
<span v-if="dashboardData.isHistoricalMetrics" class="text-yellow-600">
|
||||
@@ -618,6 +618,22 @@ function createUsageTrendChart() {
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
itemSort: function(a, b) {
|
||||
// 按值倒序排列,费用和请求数特殊处理
|
||||
const aLabel = a.dataset.label || ''
|
||||
const bLabel = b.dataset.label || ''
|
||||
|
||||
// 费用和请求数使用不同的轴,单独处理
|
||||
if (aLabel === '费用 (USD)' || bLabel === '费用 (USD)') {
|
||||
return aLabel === '费用 (USD)' ? -1 : 1
|
||||
}
|
||||
if (aLabel === '请求数' || bLabel === '请求数') {
|
||||
return aLabel === '请求数' ? 1 : -1
|
||||
}
|
||||
|
||||
// 其他按token值倒序
|
||||
return b.parsed.y - a.parsed.y
|
||||
},
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.dataset.label || ''
|
||||
@@ -633,7 +649,14 @@ function createUsageTrendChart() {
|
||||
} else if (label === '请求数') {
|
||||
return label + ': ' + value.toLocaleString() + ' 次'
|
||||
} else {
|
||||
return label + ': ' + value.toLocaleString() + ' tokens'
|
||||
// 格式化token数显示
|
||||
if (value >= 1000000) {
|
||||
return label + ': ' + (value / 1000000).toFixed(2) + 'M tokens'
|
||||
} else if (value >= 1000) {
|
||||
return label + ': ' + (value / 1000).toFixed(2) + 'K tokens'
|
||||
} else {
|
||||
return label + ': ' + value.toLocaleString() + ' tokens'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -781,12 +804,52 @@ function createApiKeysUsageTrendChart() {
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
itemSort: function(a, b) {
|
||||
// 按值倒序排列
|
||||
return b.parsed.y - a.parsed.y
|
||||
},
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.dataset.label || ''
|
||||
const value = context.parsed.y
|
||||
const unit = apiKeysTrendMetric.value === 'tokens' ? ' tokens' : ' 次'
|
||||
return label + ': ' + value.toLocaleString() + unit
|
||||
const dataIndex = context.dataIndex
|
||||
const dataPoint = apiKeysTrendData.value.data[dataIndex]
|
||||
|
||||
// 获取所有数据集在这个时间点的值,用于排名
|
||||
const allValues = context.chart.data.datasets.map((dataset, idx) => ({
|
||||
value: dataset.data[dataIndex] || 0,
|
||||
index: idx
|
||||
})).sort((a, b) => b.value - a.value)
|
||||
|
||||
// 找出当前数据集的排名
|
||||
const rank = allValues.findIndex(item => item.index === context.datasetIndex) + 1
|
||||
|
||||
// 准备排名标识
|
||||
let rankIcon = ''
|
||||
if (rank === 1) rankIcon = '🥇 '
|
||||
else if (rank === 2) rankIcon = '🥈 '
|
||||
else if (rank === 3) rankIcon = '🥉 '
|
||||
|
||||
if (apiKeysTrendMetric.value === 'tokens') {
|
||||
// 格式化token显示
|
||||
let formattedValue = ''
|
||||
if (value >= 1000000) {
|
||||
formattedValue = (value / 1000000).toFixed(2) + 'M'
|
||||
} else if (value >= 1000) {
|
||||
formattedValue = (value / 1000).toFixed(2) + 'K'
|
||||
} else {
|
||||
formattedValue = value.toLocaleString()
|
||||
}
|
||||
|
||||
// 获取对应API Key的费用信息
|
||||
const apiKeyId = apiKeysTrendData.value.topApiKeys[context.datasetIndex]
|
||||
const apiKeyData = dataPoint?.apiKeys?.[apiKeyId]
|
||||
const cost = apiKeyData?.formattedCost || '$0.00'
|
||||
|
||||
return `${rankIcon}${label}: ${formattedValue} tokens (${cost})`
|
||||
} else {
|
||||
return `${rankIcon}${label}: ${value.toLocaleString()} 次`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user