mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 实现基于滑动窗口的实时RPM/TPM统计
- 添加系统级分钟统计,支持1-60分钟可配置时间窗口 - 新增 getRealtimeSystemMetrics 方法计算滑动窗口内的平均值 - 前端显示实时RPM/TPM,标注时间窗口和数据来源 - 修复 EditApiKeyModal 中模型限制和客户端限制复选框状态错误 - 优化性能:使用Pipeline批量操作替代Promise.all - TPM包含所有token类型:input、output、cache_creation、cache_read - 添加降级方案:实时数据不可用时返回历史平均值 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,7 @@ TOKEN_USAGE_RETENTION=2592000000
|
|||||||
HEALTH_CHECK_INTERVAL=60000
|
HEALTH_CHECK_INTERVAL=60000
|
||||||
SYSTEM_TIMEZONE=Asia/Shanghai
|
SYSTEM_TIMEZONE=Asia/Shanghai
|
||||||
TIMEZONE_OFFSET=8
|
TIMEZONE_OFFSET=8
|
||||||
|
METRICS_WINDOW=5 # 实时指标统计窗口(分钟),可选1-60,默认5分钟
|
||||||
|
|
||||||
# 🎨 Web 界面配置
|
# 🎨 Web 界面配置
|
||||||
WEB_TITLE=Claude Relay Service
|
WEB_TITLE=Claude Relay Service
|
||||||
|
|||||||
@@ -186,6 +186,10 @@ class RedisClient {
|
|||||||
const keyModelMonthly = `usage:${keyId}:model:monthly:${model}:${currentMonth}`;
|
const keyModelMonthly = `usage:${keyId}:model:monthly:${model}:${currentMonth}`;
|
||||||
const keyModelHourly = `usage:${keyId}:model:hourly:${model}:${currentHour}`; // 新增API Key模型小时级别
|
const keyModelHourly = `usage:${keyId}:model:hourly:${model}:${currentHour}`; // 新增API Key模型小时级别
|
||||||
|
|
||||||
|
// 新增:系统级分钟统计
|
||||||
|
const minuteTimestamp = Math.floor(now.getTime() / 60000);
|
||||||
|
const systemMinuteKey = `system:metrics:minute:${minuteTimestamp}`;
|
||||||
|
|
||||||
// 智能处理输入输出token分配
|
// 智能处理输入输出token分配
|
||||||
const finalInputTokens = inputTokens || 0;
|
const finalInputTokens = inputTokens || 0;
|
||||||
const finalOutputTokens = outputTokens || (finalInputTokens > 0 ? 0 : tokens);
|
const finalOutputTokens = outputTokens || (finalInputTokens > 0 ? 0 : tokens);
|
||||||
@@ -197,96 +201,122 @@ class RedisClient {
|
|||||||
// 核心token(不包括缓存)- 用于与历史数据兼容
|
// 核心token(不包括缓存)- 用于与历史数据兼容
|
||||||
const coreTokens = finalInputTokens + finalOutputTokens;
|
const coreTokens = finalInputTokens + finalOutputTokens;
|
||||||
|
|
||||||
await Promise.all([
|
// 使用Pipeline优化性能
|
||||||
// 核心token统计(保持向后兼容)
|
const pipeline = this.client.pipeline();
|
||||||
this.client.hincrby(key, 'totalTokens', coreTokens),
|
|
||||||
this.client.hincrby(key, 'totalInputTokens', finalInputTokens),
|
// 现有的统计保持不变
|
||||||
this.client.hincrby(key, 'totalOutputTokens', finalOutputTokens),
|
// 核心token统计(保持向后兼容)
|
||||||
// 缓存token统计(新增)
|
pipeline.hincrby(key, 'totalTokens', coreTokens);
|
||||||
this.client.hincrby(key, 'totalCacheCreateTokens', finalCacheCreateTokens),
|
pipeline.hincrby(key, 'totalInputTokens', finalInputTokens);
|
||||||
this.client.hincrby(key, 'totalCacheReadTokens', finalCacheReadTokens),
|
pipeline.hincrby(key, 'totalOutputTokens', finalOutputTokens);
|
||||||
this.client.hincrby(key, 'totalAllTokens', totalTokens), // 包含所有类型的总token
|
// 缓存token统计(新增)
|
||||||
// 请求计数
|
pipeline.hincrby(key, 'totalCacheCreateTokens', finalCacheCreateTokens);
|
||||||
this.client.hincrby(key, 'totalRequests', 1),
|
pipeline.hincrby(key, 'totalCacheReadTokens', finalCacheReadTokens);
|
||||||
// 每日统计
|
pipeline.hincrby(key, 'totalAllTokens', totalTokens); // 包含所有类型的总token
|
||||||
this.client.hincrby(daily, 'tokens', coreTokens),
|
// 请求计数
|
||||||
this.client.hincrby(daily, 'inputTokens', finalInputTokens),
|
pipeline.hincrby(key, 'totalRequests', 1);
|
||||||
this.client.hincrby(daily, 'outputTokens', finalOutputTokens),
|
|
||||||
this.client.hincrby(daily, 'cacheCreateTokens', finalCacheCreateTokens),
|
// 每日统计
|
||||||
this.client.hincrby(daily, 'cacheReadTokens', finalCacheReadTokens),
|
pipeline.hincrby(daily, 'tokens', coreTokens);
|
||||||
this.client.hincrby(daily, 'allTokens', totalTokens),
|
pipeline.hincrby(daily, 'inputTokens', finalInputTokens);
|
||||||
this.client.hincrby(daily, 'requests', 1),
|
pipeline.hincrby(daily, 'outputTokens', finalOutputTokens);
|
||||||
// 每月统计
|
pipeline.hincrby(daily, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
this.client.hincrby(monthly, 'tokens', coreTokens),
|
pipeline.hincrby(daily, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
this.client.hincrby(monthly, 'inputTokens', finalInputTokens),
|
pipeline.hincrby(daily, 'allTokens', totalTokens);
|
||||||
this.client.hincrby(monthly, 'outputTokens', finalOutputTokens),
|
pipeline.hincrby(daily, 'requests', 1);
|
||||||
this.client.hincrby(monthly, 'cacheCreateTokens', finalCacheCreateTokens),
|
|
||||||
this.client.hincrby(monthly, 'cacheReadTokens', finalCacheReadTokens),
|
// 每月统计
|
||||||
this.client.hincrby(monthly, 'allTokens', totalTokens),
|
pipeline.hincrby(monthly, 'tokens', coreTokens);
|
||||||
this.client.hincrby(monthly, 'requests', 1),
|
pipeline.hincrby(monthly, 'inputTokens', finalInputTokens);
|
||||||
// 按模型统计 - 每日
|
pipeline.hincrby(monthly, 'outputTokens', finalOutputTokens);
|
||||||
this.client.hincrby(modelDaily, 'inputTokens', finalInputTokens),
|
pipeline.hincrby(monthly, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
this.client.hincrby(modelDaily, 'outputTokens', finalOutputTokens),
|
pipeline.hincrby(monthly, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
this.client.hincrby(modelDaily, 'cacheCreateTokens', finalCacheCreateTokens),
|
pipeline.hincrby(monthly, 'allTokens', totalTokens);
|
||||||
this.client.hincrby(modelDaily, 'cacheReadTokens', finalCacheReadTokens),
|
pipeline.hincrby(monthly, 'requests', 1);
|
||||||
this.client.hincrby(modelDaily, 'allTokens', totalTokens),
|
|
||||||
this.client.hincrby(modelDaily, 'requests', 1),
|
// 按模型统计 - 每日
|
||||||
// 按模型统计 - 每月
|
pipeline.hincrby(modelDaily, 'inputTokens', finalInputTokens);
|
||||||
this.client.hincrby(modelMonthly, 'inputTokens', finalInputTokens),
|
pipeline.hincrby(modelDaily, 'outputTokens', finalOutputTokens);
|
||||||
this.client.hincrby(modelMonthly, 'outputTokens', finalOutputTokens),
|
pipeline.hincrby(modelDaily, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
this.client.hincrby(modelMonthly, 'cacheCreateTokens', finalCacheCreateTokens),
|
pipeline.hincrby(modelDaily, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
this.client.hincrby(modelMonthly, 'cacheReadTokens', finalCacheReadTokens),
|
pipeline.hincrby(modelDaily, 'allTokens', totalTokens);
|
||||||
this.client.hincrby(modelMonthly, 'allTokens', totalTokens),
|
pipeline.hincrby(modelDaily, 'requests', 1);
|
||||||
this.client.hincrby(modelMonthly, 'requests', 1),
|
|
||||||
// API Key级别的模型统计 - 每日
|
// 按模型统计 - 每月
|
||||||
this.client.hincrby(keyModelDaily, 'inputTokens', finalInputTokens),
|
pipeline.hincrby(modelMonthly, 'inputTokens', finalInputTokens);
|
||||||
this.client.hincrby(keyModelDaily, 'outputTokens', finalOutputTokens),
|
pipeline.hincrby(modelMonthly, 'outputTokens', finalOutputTokens);
|
||||||
this.client.hincrby(keyModelDaily, 'cacheCreateTokens', finalCacheCreateTokens),
|
pipeline.hincrby(modelMonthly, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
this.client.hincrby(keyModelDaily, 'cacheReadTokens', finalCacheReadTokens),
|
pipeline.hincrby(modelMonthly, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
this.client.hincrby(keyModelDaily, 'allTokens', totalTokens),
|
pipeline.hincrby(modelMonthly, 'allTokens', totalTokens);
|
||||||
this.client.hincrby(keyModelDaily, 'requests', 1),
|
pipeline.hincrby(modelMonthly, 'requests', 1);
|
||||||
// API Key级别的模型统计 - 每月
|
|
||||||
this.client.hincrby(keyModelMonthly, 'inputTokens', finalInputTokens),
|
// API Key级别的模型统计 - 每日
|
||||||
this.client.hincrby(keyModelMonthly, 'outputTokens', finalOutputTokens),
|
pipeline.hincrby(keyModelDaily, 'inputTokens', finalInputTokens);
|
||||||
this.client.hincrby(keyModelMonthly, 'cacheCreateTokens', finalCacheCreateTokens),
|
pipeline.hincrby(keyModelDaily, 'outputTokens', finalOutputTokens);
|
||||||
this.client.hincrby(keyModelMonthly, 'cacheReadTokens', finalCacheReadTokens),
|
pipeline.hincrby(keyModelDaily, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
this.client.hincrby(keyModelMonthly, 'allTokens', totalTokens),
|
pipeline.hincrby(keyModelDaily, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
this.client.hincrby(keyModelMonthly, 'requests', 1),
|
pipeline.hincrby(keyModelDaily, 'allTokens', totalTokens);
|
||||||
|
pipeline.hincrby(keyModelDaily, 'requests', 1);
|
||||||
// 小时级别统计
|
|
||||||
this.client.hincrby(hourly, 'tokens', coreTokens),
|
// API Key级别的模型统计 - 每月
|
||||||
this.client.hincrby(hourly, 'inputTokens', finalInputTokens),
|
pipeline.hincrby(keyModelMonthly, 'inputTokens', finalInputTokens);
|
||||||
this.client.hincrby(hourly, 'outputTokens', finalOutputTokens),
|
pipeline.hincrby(keyModelMonthly, 'outputTokens', finalOutputTokens);
|
||||||
this.client.hincrby(hourly, 'cacheCreateTokens', finalCacheCreateTokens),
|
pipeline.hincrby(keyModelMonthly, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
this.client.hincrby(hourly, 'cacheReadTokens', finalCacheReadTokens),
|
pipeline.hincrby(keyModelMonthly, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
this.client.hincrby(hourly, 'allTokens', totalTokens),
|
pipeline.hincrby(keyModelMonthly, 'allTokens', totalTokens);
|
||||||
this.client.hincrby(hourly, 'requests', 1),
|
pipeline.hincrby(keyModelMonthly, 'requests', 1);
|
||||||
// 按模型统计 - 每小时
|
|
||||||
this.client.hincrby(modelHourly, 'inputTokens', finalInputTokens),
|
// 小时级别统计
|
||||||
this.client.hincrby(modelHourly, 'outputTokens', finalOutputTokens),
|
pipeline.hincrby(hourly, 'tokens', coreTokens);
|
||||||
this.client.hincrby(modelHourly, 'cacheCreateTokens', finalCacheCreateTokens),
|
pipeline.hincrby(hourly, 'inputTokens', finalInputTokens);
|
||||||
this.client.hincrby(modelHourly, 'cacheReadTokens', finalCacheReadTokens),
|
pipeline.hincrby(hourly, 'outputTokens', finalOutputTokens);
|
||||||
this.client.hincrby(modelHourly, 'allTokens', totalTokens),
|
pipeline.hincrby(hourly, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
this.client.hincrby(modelHourly, 'requests', 1),
|
pipeline.hincrby(hourly, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
// API Key级别的模型统计 - 每小时
|
pipeline.hincrby(hourly, 'allTokens', totalTokens);
|
||||||
this.client.hincrby(keyModelHourly, 'inputTokens', finalInputTokens),
|
pipeline.hincrby(hourly, 'requests', 1);
|
||||||
this.client.hincrby(keyModelHourly, 'outputTokens', finalOutputTokens),
|
|
||||||
this.client.hincrby(keyModelHourly, 'cacheCreateTokens', finalCacheCreateTokens),
|
// 按模型统计 - 每小时
|
||||||
this.client.hincrby(keyModelHourly, 'cacheReadTokens', finalCacheReadTokens),
|
pipeline.hincrby(modelHourly, 'inputTokens', finalInputTokens);
|
||||||
this.client.hincrby(keyModelHourly, 'allTokens', totalTokens),
|
pipeline.hincrby(modelHourly, 'outputTokens', finalOutputTokens);
|
||||||
this.client.hincrby(keyModelHourly, 'requests', 1),
|
pipeline.hincrby(modelHourly, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
|
pipeline.hincrby(modelHourly, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
// 设置过期时间
|
pipeline.hincrby(modelHourly, 'allTokens', totalTokens);
|
||||||
this.client.expire(daily, 86400 * 32), // 32天过期
|
pipeline.hincrby(modelHourly, 'requests', 1);
|
||||||
this.client.expire(monthly, 86400 * 365), // 1年过期
|
|
||||||
this.client.expire(hourly, 86400 * 7), // 小时统计7天过期
|
// API Key级别的模型统计 - 每小时
|
||||||
this.client.expire(modelDaily, 86400 * 32), // 模型每日统计32天过期
|
pipeline.hincrby(keyModelHourly, 'inputTokens', finalInputTokens);
|
||||||
this.client.expire(modelMonthly, 86400 * 365), // 模型每月统计1年过期
|
pipeline.hincrby(keyModelHourly, 'outputTokens', finalOutputTokens);
|
||||||
this.client.expire(modelHourly, 86400 * 7), // 模型小时统计7天过期
|
pipeline.hincrby(keyModelHourly, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
this.client.expire(keyModelDaily, 86400 * 32), // API Key模型每日统计32天过期
|
pipeline.hincrby(keyModelHourly, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
this.client.expire(keyModelMonthly, 86400 * 365), // API Key模型每月统计1年过期
|
pipeline.hincrby(keyModelHourly, 'allTokens', totalTokens);
|
||||||
this.client.expire(keyModelHourly, 86400 * 7) // API Key模型小时统计7天过期
|
pipeline.hincrby(keyModelHourly, 'requests', 1);
|
||||||
]);
|
|
||||||
|
// 新增:系统级分钟统计
|
||||||
|
pipeline.hincrby(systemMinuteKey, 'requests', 1);
|
||||||
|
pipeline.hincrby(systemMinuteKey, 'totalTokens', totalTokens);
|
||||||
|
pipeline.hincrby(systemMinuteKey, 'inputTokens', finalInputTokens);
|
||||||
|
pipeline.hincrby(systemMinuteKey, 'outputTokens', finalOutputTokens);
|
||||||
|
pipeline.hincrby(systemMinuteKey, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
|
pipeline.hincrby(systemMinuteKey, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
|
|
||||||
|
// 设置过期时间
|
||||||
|
pipeline.expire(daily, 86400 * 32); // 32天过期
|
||||||
|
pipeline.expire(monthly, 86400 * 365); // 1年过期
|
||||||
|
pipeline.expire(hourly, 86400 * 7); // 小时统计7天过期
|
||||||
|
pipeline.expire(modelDaily, 86400 * 32); // 模型每日统计32天过期
|
||||||
|
pipeline.expire(modelMonthly, 86400 * 365); // 模型每月统计1年过期
|
||||||
|
pipeline.expire(modelHourly, 86400 * 7); // 模型小时统计7天过期
|
||||||
|
pipeline.expire(keyModelDaily, 86400 * 32); // API Key模型每日统计32天过期
|
||||||
|
pipeline.expire(keyModelMonthly, 86400 * 365); // API Key模型每月统计1年过期
|
||||||
|
pipeline.expire(keyModelHourly, 86400 * 7); // API Key模型小时统计7天过期
|
||||||
|
|
||||||
|
// 系统级分钟统计的过期时间(窗口时间的2倍)
|
||||||
|
const config = require('../../config/config');
|
||||||
|
const metricsWindow = config.system.metricsWindow;
|
||||||
|
pipeline.expire(systemMinuteKey, metricsWindow * 60 * 2);
|
||||||
|
|
||||||
|
// 执行Pipeline
|
||||||
|
await pipeline.exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 📊 记录账户级别的使用统计
|
// 📊 记录账户级别的使用统计
|
||||||
@@ -974,6 +1004,76 @@ class RedisClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 📊 获取实时系统指标(基于滑动窗口)
|
||||||
|
async getRealtimeSystemMetrics() {
|
||||||
|
try {
|
||||||
|
const config = require('../../config/config');
|
||||||
|
const windowMinutes = config.system.metricsWindow;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const currentMinute = Math.floor(now.getTime() / 60000);
|
||||||
|
|
||||||
|
// 使用Pipeline批量获取窗口内的所有分钟数据
|
||||||
|
const pipeline = this.client.pipeline();
|
||||||
|
for (let i = 0; i < windowMinutes; i++) {
|
||||||
|
const minuteKey = `system:metrics:minute:${currentMinute - i}`;
|
||||||
|
pipeline.hgetall(minuteKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await pipeline.exec();
|
||||||
|
|
||||||
|
// 聚合计算
|
||||||
|
let totalRequests = 0;
|
||||||
|
let totalTokens = 0;
|
||||||
|
let totalInputTokens = 0;
|
||||||
|
let totalOutputTokens = 0;
|
||||||
|
let totalCacheCreateTokens = 0;
|
||||||
|
let totalCacheReadTokens = 0;
|
||||||
|
|
||||||
|
results.forEach(([err, data]) => {
|
||||||
|
if (!err && data) {
|
||||||
|
totalRequests += parseInt(data.requests || 0);
|
||||||
|
totalTokens += parseInt(data.totalTokens || 0);
|
||||||
|
totalInputTokens += parseInt(data.inputTokens || 0);
|
||||||
|
totalOutputTokens += parseInt(data.outputTokens || 0);
|
||||||
|
totalCacheCreateTokens += parseInt(data.cacheCreateTokens || 0);
|
||||||
|
totalCacheReadTokens += parseInt(data.cacheReadTokens || 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算平均值(每分钟)
|
||||||
|
const realtimeRPM = windowMinutes > 0 ? Math.round((totalRequests / windowMinutes) * 100) / 100 : 0;
|
||||||
|
const realtimeTPM = windowMinutes > 0 ? Math.round((totalTokens / windowMinutes) * 100) / 100 : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
realtimeRPM,
|
||||||
|
realtimeTPM,
|
||||||
|
windowMinutes,
|
||||||
|
totalRequests,
|
||||||
|
totalTokens,
|
||||||
|
totalInputTokens,
|
||||||
|
totalOutputTokens,
|
||||||
|
totalCacheCreateTokens,
|
||||||
|
totalCacheReadTokens
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting realtime system metrics:', error);
|
||||||
|
// 如果出错,返回历史平均值作为降级方案
|
||||||
|
const historicalMetrics = await this.getSystemAverages();
|
||||||
|
return {
|
||||||
|
realtimeRPM: historicalMetrics.systemRPM,
|
||||||
|
realtimeTPM: historicalMetrics.systemTPM,
|
||||||
|
windowMinutes: 0, // 标识使用了历史数据
|
||||||
|
totalRequests: 0,
|
||||||
|
totalTokens: historicalMetrics.totalTokens,
|
||||||
|
totalInputTokens: historicalMetrics.totalInputTokens,
|
||||||
|
totalOutputTokens: historicalMetrics.totalOutputTokens,
|
||||||
|
totalCacheCreateTokens: 0,
|
||||||
|
totalCacheReadTokens: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 🔗 会话sticky映射管理
|
// 🔗 会话sticky映射管理
|
||||||
async setSessionAccountMapping(sessionHash, accountId, ttl = 3600) {
|
async setSessionAccountMapping(sessionHash, accountId, ttl = 3600) {
|
||||||
const key = `sticky_session:${sessionHash}`;
|
const key = `sticky_session:${sessionHash}`;
|
||||||
|
|||||||
@@ -1261,13 +1261,14 @@ router.get('/accounts/:accountId/usage-stats', authenticateAdmin, async (req, re
|
|||||||
// 获取系统概览
|
// 获取系统概览
|
||||||
router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [, apiKeys, claudeAccounts, geminiAccounts, todayStats, systemAverages] = await Promise.all([
|
const [, apiKeys, claudeAccounts, geminiAccounts, todayStats, systemAverages, realtimeMetrics] = await Promise.all([
|
||||||
redis.getSystemStats(),
|
redis.getSystemStats(),
|
||||||
apiKeyService.getAllApiKeys(),
|
apiKeyService.getAllApiKeys(),
|
||||||
claudeAccountService.getAllAccounts(),
|
claudeAccountService.getAllAccounts(),
|
||||||
geminiAccountService.getAllAccounts(),
|
geminiAccountService.getAllAccounts(),
|
||||||
redis.getTodayStats(),
|
redis.getTodayStats(),
|
||||||
redis.getSystemAverages()
|
redis.getSystemAverages(),
|
||||||
|
redis.getRealtimeSystemMetrics()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 计算使用统计(统一使用allTokens)
|
// 计算使用统计(统一使用allTokens)
|
||||||
@@ -1316,6 +1317,12 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
|||||||
rpm: systemAverages.systemRPM,
|
rpm: systemAverages.systemRPM,
|
||||||
tpm: systemAverages.systemTPM
|
tpm: systemAverages.systemTPM
|
||||||
},
|
},
|
||||||
|
realtimeMetrics: {
|
||||||
|
rpm: realtimeMetrics.realtimeRPM,
|
||||||
|
tpm: realtimeMetrics.realtimeTPM,
|
||||||
|
windowMinutes: realtimeMetrics.windowMinutes,
|
||||||
|
isHistorical: realtimeMetrics.windowMinutes === 0 // 标识是否使用了历史数据
|
||||||
|
},
|
||||||
systemHealth: {
|
systemHealth: {
|
||||||
redisConnected: redis.isConnected,
|
redisConnected: redis.isConnected,
|
||||||
claudeAccountsHealthy: activeClaudeAccounts > 0,
|
claudeAccountsHealthy: activeClaudeAccounts > 0,
|
||||||
@@ -1483,7 +1490,7 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
endTime = new Date(endDate);
|
endTime = new Date(endDate);
|
||||||
|
|
||||||
// 调试日志
|
// 调试日志
|
||||||
logger.info(`📊 Usage trend hour granularity - received times:`);
|
logger.info('📊 Usage trend hour granularity - received times:');
|
||||||
logger.info(` startDate (raw): ${startDate}`);
|
logger.info(` startDate (raw): ${startDate}`);
|
||||||
logger.info(` endDate (raw): ${endDate}`);
|
logger.info(` endDate (raw): ${endDate}`);
|
||||||
logger.info(` startTime (parsed): ${startTime.toISOString()}`);
|
logger.info(` startTime (parsed): ${startTime.toISOString()}`);
|
||||||
|
|||||||
@@ -88,11 +88,11 @@ class ClaudeConsoleRelayService {
|
|||||||
logger.debug(`[DEBUG] Adding beta header: ${options.betaHeader}`);
|
logger.debug(`[DEBUG] Adding beta header: ${options.betaHeader}`);
|
||||||
requestConfig.headers['anthropic-beta'] = options.betaHeader;
|
requestConfig.headers['anthropic-beta'] = options.betaHeader;
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`[DEBUG] No beta header to add`);
|
logger.debug('[DEBUG] No beta header to add');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送请求
|
// 发送请求
|
||||||
logger.debug(`📤 Sending request to Claude Console API with headers:`, JSON.stringify(requestConfig.headers, null, 2));
|
logger.debug('📤 Sending request to Claude Console API with headers:', JSON.stringify(requestConfig.headers, null, 2));
|
||||||
const response = await axios(requestConfig);
|
const response = await axios(requestConfig);
|
||||||
|
|
||||||
// 移除监听器(请求成功完成)
|
// 移除监听器(请求成功完成)
|
||||||
|
|||||||
@@ -522,8 +522,9 @@ onMounted(async () => {
|
|||||||
form.restrictedModels = props.apiKey.restrictedModels || []
|
form.restrictedModels = props.apiKey.restrictedModels || []
|
||||||
form.allowedClients = props.apiKey.allowedClients || []
|
form.allowedClients = props.apiKey.allowedClients || []
|
||||||
form.tags = props.apiKey.tags || []
|
form.tags = props.apiKey.tags || []
|
||||||
form.enableModelRestriction = form.restrictedModels.length > 0
|
// 从后端数据中获取实际的启用状态,而不是根据数组长度推断
|
||||||
form.enableClientRestriction = form.allowedClients.length > 0
|
form.enableModelRestriction = props.apiKey.enableModelRestriction || false
|
||||||
|
form.enableClientRestriction = props.apiKey.enableClientRestriction || false
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ export const useDashboardStore = defineStore('dashboard', () => {
|
|||||||
todayCacheReadTokens: 0,
|
todayCacheReadTokens: 0,
|
||||||
systemRPM: 0,
|
systemRPM: 0,
|
||||||
systemTPM: 0,
|
systemTPM: 0,
|
||||||
|
realtimeRPM: 0,
|
||||||
|
realtimeTPM: 0,
|
||||||
|
metricsWindow: 5,
|
||||||
|
isHistoricalMetrics: false,
|
||||||
systemStatus: '正常',
|
systemStatus: '正常',
|
||||||
uptime: 0,
|
uptime: 0,
|
||||||
systemTimezone: 8 // 默认 UTC+8
|
systemTimezone: 8 // 默认 UTC+8
|
||||||
@@ -129,6 +133,7 @@ export const useDashboardStore = defineStore('dashboard', () => {
|
|||||||
const overview = dashboardResponse.data.overview || {}
|
const overview = dashboardResponse.data.overview || {}
|
||||||
const recentActivity = dashboardResponse.data.recentActivity || {}
|
const recentActivity = dashboardResponse.data.recentActivity || {}
|
||||||
const systemAverages = dashboardResponse.data.systemAverages || {}
|
const systemAverages = dashboardResponse.data.systemAverages || {}
|
||||||
|
const realtimeMetrics = dashboardResponse.data.realtimeMetrics || {}
|
||||||
const systemHealth = dashboardResponse.data.systemHealth || {}
|
const systemHealth = dashboardResponse.data.systemHealth || {}
|
||||||
|
|
||||||
dashboardData.value = {
|
dashboardData.value = {
|
||||||
@@ -151,6 +156,10 @@ export const useDashboardStore = defineStore('dashboard', () => {
|
|||||||
todayCacheReadTokens: recentActivity.cacheReadTokensToday || 0,
|
todayCacheReadTokens: recentActivity.cacheReadTokensToday || 0,
|
||||||
systemRPM: systemAverages.rpm || 0,
|
systemRPM: systemAverages.rpm || 0,
|
||||||
systemTPM: systemAverages.tpm || 0,
|
systemTPM: systemAverages.tpm || 0,
|
||||||
|
realtimeRPM: realtimeMetrics.rpm || 0,
|
||||||
|
realtimeTPM: realtimeMetrics.tpm || 0,
|
||||||
|
metricsWindow: realtimeMetrics.windowMinutes || 5,
|
||||||
|
isHistoricalMetrics: realtimeMetrics.isHistorical || false,
|
||||||
systemStatus: systemHealth.redisConnected ? '正常' : '异常',
|
systemStatus: systemHealth.redisConnected ? '正常' : '异常',
|
||||||
uptime: systemHealth.uptime || 0,
|
uptime: systemHealth.uptime || 0,
|
||||||
systemTimezone: dashboardResponse.data.systemTimezone || 8
|
systemTimezone: dashboardResponse.data.systemTimezone || 8
|
||||||
|
|||||||
@@ -111,9 +111,17 @@
|
|||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold text-gray-600 mb-1">平均RPM</p>
|
<p class="text-sm font-semibold text-gray-600 mb-1">
|
||||||
<p class="text-3xl font-bold text-orange-600">{{ dashboardData.systemRPM || 0 }}</p>
|
实时RPM
|
||||||
<p class="text-xs text-gray-500 mt-1">每分钟请求数</p>
|
<span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}分钟)</span>
|
||||||
|
</p>
|
||||||
|
<p class="text-3xl font-bold text-orange-600">{{ dashboardData.realtimeRPM || 0 }}</p>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
|
每分钟请求数
|
||||||
|
<span v-if="dashboardData.isHistoricalMetrics" class="text-yellow-600">
|
||||||
|
<i class="fas fa-exclamation-circle"></i> 历史数据
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-orange-500 to-orange-600">
|
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-orange-500 to-orange-600">
|
||||||
<i class="fas fa-tachometer-alt"></i>
|
<i class="fas fa-tachometer-alt"></i>
|
||||||
@@ -124,9 +132,17 @@
|
|||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold text-gray-600 mb-1">平均TPM</p>
|
<p class="text-sm font-semibold text-gray-600 mb-1">
|
||||||
<p class="text-3xl font-bold text-rose-600">{{ dashboardData.systemTPM || 0 }}</p>
|
实时TPM
|
||||||
<p class="text-xs text-gray-500 mt-1">每分钟Token数</p>
|
<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-xs text-gray-500 mt-1">
|
||||||
|
每分钟Token数
|
||||||
|
<span v-if="dashboardData.isHistoricalMetrics" class="text-yellow-600">
|
||||||
|
<i class="fas fa-exclamation-circle"></i> 历史数据
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-rose-500 to-rose-600">
|
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-rose-500 to-rose-600">
|
||||||
<i class="fas fa-rocket"></i>
|
<i class="fas fa-rocket"></i>
|
||||||
|
|||||||
Reference in New Issue
Block a user