first commit

This commit is contained in:
shaw
2025-07-14 18:14:13 +08:00
parent a96a372011
commit b1ca3f307e
31 changed files with 20046 additions and 21 deletions

901
src/routes/admin.js Normal file
View File

@@ -0,0 +1,901 @@
const express = require('express');
const apiKeyService = require('../services/apiKeyService');
const claudeAccountService = require('../services/claudeAccountService');
const redis = require('../models/redis');
const { authenticateAdmin } = require('../middleware/auth');
const logger = require('../utils/logger');
const oauthHelper = require('../utils/oauthHelper');
const CostCalculator = require('../utils/costCalculator');
const pricingService = require('../services/pricingService');
const router = express.Router();
// 🔑 API Keys 管理
// 获取所有API Keys
router.get('/api-keys', authenticateAdmin, async (req, res) => {
try {
const apiKeys = await apiKeyService.getAllApiKeys();
res.json({ success: true, data: apiKeys });
} catch (error) {
logger.error('❌ Failed to get API keys:', error);
res.status(500).json({ error: 'Failed to get API keys', message: error.message });
}
});
// 创建新的API Key
router.post('/api-keys', authenticateAdmin, async (req, res) => {
try {
const {
name,
description,
tokenLimit,
requestLimit,
expiresAt,
claudeAccountId
} = req.body;
// 输入验证
if (!name || typeof name !== 'string' || name.trim().length === 0) {
return res.status(400).json({ error: 'Name is required and must be a non-empty string' });
}
if (name.length > 100) {
return res.status(400).json({ error: 'Name must be less than 100 characters' });
}
if (description && (typeof description !== 'string' || description.length > 500)) {
return res.status(400).json({ error: 'Description must be a string with less than 500 characters' });
}
if (tokenLimit && (!Number.isInteger(Number(tokenLimit)) || Number(tokenLimit) < 0)) {
return res.status(400).json({ error: 'Token limit must be a non-negative integer' });
}
if (requestLimit && (!Number.isInteger(Number(requestLimit)) || Number(requestLimit) < 0)) {
return res.status(400).json({ error: 'Request limit must be a non-negative integer' });
}
const newKey = await apiKeyService.generateApiKey({
name,
description,
tokenLimit,
requestLimit,
expiresAt,
claudeAccountId
});
logger.success(`🔑 Admin created new API key: ${name}`);
res.json({ success: true, data: newKey });
} catch (error) {
logger.error('❌ Failed to create API key:', error);
res.status(500).json({ error: 'Failed to create API key', message: error.message });
}
});
// 更新API Key
router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
try {
const { keyId } = req.params;
const updates = req.body;
await apiKeyService.updateApiKey(keyId, updates);
logger.success(`📝 Admin updated API key: ${keyId}`);
res.json({ success: true, message: 'API key updated successfully' });
} catch (error) {
logger.error('❌ Failed to update API key:', error);
res.status(500).json({ error: 'Failed to update API key', message: error.message });
}
});
// 删除API Key
router.delete('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
try {
const { keyId } = req.params;
await apiKeyService.deleteApiKey(keyId);
logger.success(`🗑️ Admin deleted API key: ${keyId}`);
res.json({ success: true, message: 'API key deleted successfully' });
} catch (error) {
logger.error('❌ Failed to delete API key:', error);
res.status(500).json({ error: 'Failed to delete API key', message: error.message });
}
});
// 🏢 Claude 账户管理
// 生成OAuth授权URL
router.post('/claude-accounts/generate-auth-url', authenticateAdmin, async (req, res) => {
try {
const { proxy } = req.body; // 接收代理配置
const oauthParams = await oauthHelper.generateOAuthParams();
// 将codeVerifier和state临时存储到Redis用于后续验证
const sessionId = require('crypto').randomUUID();
await redis.setOAuthSession(sessionId, {
codeVerifier: oauthParams.codeVerifier,
state: oauthParams.state,
codeChallenge: oauthParams.codeChallenge,
proxy: proxy || null, // 存储代理配置
createdAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString() // 10分钟过期
});
logger.success('🔗 Generated OAuth authorization URL with proxy support');
res.json({
success: true,
data: {
authUrl: oauthParams.authUrl,
sessionId: sessionId,
instructions: [
'1. 复制上面的链接到浏览器中打开',
'2. 登录您的 Anthropic 账户',
'3. 同意应用权限',
'4. 复制浏览器地址栏中的完整 URL',
'5. 在添加账户表单中粘贴完整的回调 URL 和授权码'
]
}
});
} catch (error) {
logger.error('❌ Failed to generate OAuth URL:', error);
res.status(500).json({ error: 'Failed to generate OAuth URL', message: error.message });
}
});
// 验证授权码并获取token
router.post('/claude-accounts/exchange-code', authenticateAdmin, async (req, res) => {
try {
const { sessionId, authorizationCode, callbackUrl } = req.body;
if (!sessionId || (!authorizationCode && !callbackUrl)) {
return res.status(400).json({ error: 'Session ID and authorization code (or callback URL) are required' });
}
// 从Redis获取OAuth会话信息
const oauthSession = await redis.getOAuthSession(sessionId);
if (!oauthSession) {
return res.status(400).json({ error: 'Invalid or expired OAuth session' });
}
// 检查会话是否过期
if (new Date() > new Date(oauthSession.expiresAt)) {
await redis.deleteOAuthSession(sessionId);
return res.status(400).json({ error: 'OAuth session has expired, please generate a new authorization URL' });
}
// 统一处理授权码输入可能是直接的code或完整的回调URL
let finalAuthCode;
const inputValue = callbackUrl || authorizationCode;
try {
finalAuthCode = oauthHelper.parseCallbackUrl(inputValue);
} catch (parseError) {
return res.status(400).json({ error: 'Failed to parse authorization input', message: parseError.message });
}
// 交换访问令牌
const tokenData = await oauthHelper.exchangeCodeForTokens(
finalAuthCode,
oauthSession.codeVerifier,
oauthSession.state,
oauthSession.proxy // 传递代理配置
);
// 清理OAuth会话
await redis.deleteOAuthSession(sessionId);
logger.success('🎉 Successfully exchanged authorization code for tokens');
res.json({
success: true,
data: {
claudeAiOauth: tokenData
}
});
} catch (error) {
logger.error('❌ Failed to exchange authorization code:', {
error: error.message,
sessionId: req.body.sessionId,
// 不记录完整的授权码,只记录长度和前几个字符
codeLength: req.body.callbackUrl ? req.body.callbackUrl.length : (req.body.authorizationCode ? req.body.authorizationCode.length : 0),
codePrefix: req.body.callbackUrl ? req.body.callbackUrl.substring(0, 10) + '...' : (req.body.authorizationCode ? req.body.authorizationCode.substring(0, 10) + '...' : 'N/A')
});
res.status(500).json({ error: 'Failed to exchange authorization code', message: error.message });
}
});
// 获取所有Claude账户
router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
try {
const accounts = await claudeAccountService.getAllAccounts();
res.json({ success: true, data: accounts });
} catch (error) {
logger.error('❌ Failed to get Claude accounts:', error);
res.status(500).json({ error: 'Failed to get Claude accounts', message: error.message });
}
});
// 创建新的Claude账户
router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
try {
const {
name,
description,
email,
password,
refreshToken,
claudeAiOauth,
proxy
} = req.body;
if (!name) {
return res.status(400).json({ error: 'Name is required' });
}
const newAccount = await claudeAccountService.createAccount({
name,
description,
email,
password,
refreshToken,
claudeAiOauth,
proxy
});
logger.success(`🏢 Admin created new Claude account: ${name}`);
res.json({ success: true, data: newAccount });
} catch (error) {
logger.error('❌ Failed to create Claude account:', error);
res.status(500).json({ error: 'Failed to create Claude account', message: error.message });
}
});
// 更新Claude账户
router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params;
const updates = req.body;
await claudeAccountService.updateAccount(accountId, updates);
logger.success(`📝 Admin updated Claude account: ${accountId}`);
res.json({ success: true, message: 'Claude account updated successfully' });
} catch (error) {
logger.error('❌ Failed to update Claude account:', error);
res.status(500).json({ error: 'Failed to update Claude account', message: error.message });
}
});
// 删除Claude账户
router.delete('/claude-accounts/:accountId', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params;
await claudeAccountService.deleteAccount(accountId);
logger.success(`🗑️ Admin deleted Claude account: ${accountId}`);
res.json({ success: true, message: 'Claude account deleted successfully' });
} catch (error) {
logger.error('❌ Failed to delete Claude account:', error);
res.status(500).json({ error: 'Failed to delete Claude account', message: error.message });
}
});
// 刷新Claude账户token
router.post('/claude-accounts/:accountId/refresh', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params;
const result = await claudeAccountService.refreshAccountToken(accountId);
logger.success(`🔄 Admin refreshed token for Claude account: ${accountId}`);
res.json({ success: true, data: result });
} catch (error) {
logger.error('❌ Failed to refresh Claude account token:', error);
res.status(500).json({ error: 'Failed to refresh token', message: error.message });
}
});
// 📊 系统统计
// 获取系统概览
router.get('/dashboard', authenticateAdmin, async (req, res) => {
try {
const [, apiKeys, accounts, todayStats, systemAverages] = await Promise.all([
redis.getSystemStats(),
apiKeyService.getAllApiKeys(),
claudeAccountService.getAllAccounts(),
redis.getTodayStats(),
redis.getSystemAverages()
]);
// 计算使用统计包含cache tokens
const totalTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.tokens || 0), 0);
const totalRequestsUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.requests || 0), 0);
const totalInputTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.inputTokens || 0), 0);
const totalOutputTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.outputTokens || 0), 0);
const totalCacheCreateTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.cacheCreateTokens || 0), 0);
const totalCacheReadTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.cacheReadTokens || 0), 0);
const totalAllTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.allTokens || 0), 0);
const activeApiKeys = apiKeys.filter(key => key.isActive).length;
const activeAccounts = accounts.filter(acc => acc.isActive && acc.status === 'active').length;
const dashboard = {
overview: {
totalApiKeys: apiKeys.length,
activeApiKeys,
totalClaudeAccounts: accounts.length,
activeClaudeAccounts: activeAccounts,
totalTokensUsed,
totalRequestsUsed,
totalInputTokensUsed,
totalOutputTokensUsed,
totalCacheCreateTokensUsed,
totalCacheReadTokensUsed,
totalAllTokensUsed
},
recentActivity: {
apiKeysCreatedToday: todayStats.apiKeysCreatedToday,
requestsToday: todayStats.requestsToday,
tokensToday: todayStats.tokensToday,
inputTokensToday: todayStats.inputTokensToday,
outputTokensToday: todayStats.outputTokensToday,
cacheCreateTokensToday: todayStats.cacheCreateTokensToday || 0,
cacheReadTokensToday: todayStats.cacheReadTokensToday || 0
},
systemAverages: {
rpm: systemAverages.systemRPM,
tpm: systemAverages.systemTPM
},
systemHealth: {
redisConnected: redis.isConnected,
claudeAccountsHealthy: activeAccounts > 0,
uptime: process.uptime()
}
};
res.json({ success: true, data: dashboard });
} catch (error) {
logger.error('❌ Failed to get dashboard data:', error);
res.status(500).json({ error: 'Failed to get dashboard data', message: error.message });
}
});
// 获取使用统计
router.get('/usage-stats', authenticateAdmin, async (req, res) => {
try {
const { period = 'daily' } = req.query; // daily, monthly
// 获取基础API Key统计
const apiKeys = await apiKeyService.getAllApiKeys();
const stats = apiKeys.map(key => ({
keyId: key.id,
keyName: key.name,
usage: key.usage
}));
res.json({ success: true, data: { period, stats } });
} catch (error) {
logger.error('❌ Failed to get usage stats:', error);
res.status(500).json({ error: 'Failed to get usage stats', message: error.message });
}
});
// 获取按模型的使用统计和费用
router.get('/model-stats', authenticateAdmin, async (req, res) => {
try {
const { period = 'daily' } = req.query; // daily, monthly
const today = new Date().toISOString().split('T')[0];
const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
logger.info(`📊 Getting global model stats, period: ${period}, today: ${today}, currentMonth: ${currentMonth}`);
const client = redis.getClientSafe();
// 获取所有模型的统计数据
const pattern = period === 'daily' ? `usage:model:daily:*:${today}` : `usage:model:monthly:*:${currentMonth}`;
logger.info(`📊 Searching pattern: ${pattern}`);
const keys = await client.keys(pattern);
logger.info(`📊 Found ${keys.length} matching keys:`, keys);
const modelStats = [];
for (const key of keys) {
const match = key.match(period === 'daily' ?
/usage:model:daily:(.+):\d{4}-\d{2}-\d{2}$/ :
/usage:model:monthly:(.+):\d{4}-\d{2}$/
);
if (!match) {
logger.warn(`📊 Pattern mismatch for key: ${key}`);
continue;
}
const model = match[1];
const data = await client.hgetall(key);
logger.info(`📊 Model ${model} data:`, data);
if (data && Object.keys(data).length > 0) {
const usage = {
input_tokens: parseInt(data.inputTokens) || 0,
output_tokens: parseInt(data.outputTokens) || 0,
cache_creation_input_tokens: parseInt(data.cacheCreateTokens) || 0,
cache_read_input_tokens: parseInt(data.cacheReadTokens) || 0
};
// 计算费用
const costData = CostCalculator.calculateCost(usage, model);
modelStats.push({
model,
period,
requests: parseInt(data.requests) || 0,
inputTokens: usage.input_tokens,
outputTokens: usage.output_tokens,
cacheCreateTokens: usage.cache_creation_input_tokens,
cacheReadTokens: usage.cache_read_input_tokens,
allTokens: parseInt(data.allTokens) || 0,
usage: {
requests: parseInt(data.requests) || 0,
inputTokens: usage.input_tokens,
outputTokens: usage.output_tokens,
cacheCreateTokens: usage.cache_creation_input_tokens,
cacheReadTokens: usage.cache_read_input_tokens,
totalTokens: usage.input_tokens + usage.output_tokens + usage.cache_creation_input_tokens + usage.cache_read_input_tokens
},
costs: costData.costs,
formatted: costData.formatted,
pricing: costData.pricing
});
}
}
// 按总费用排序
modelStats.sort((a, b) => b.costs.total - a.costs.total);
logger.info(`📊 Returning ${modelStats.length} global model stats for period ${period}:`, modelStats);
res.json({ success: true, data: modelStats });
} catch (error) {
logger.error('❌ Failed to get model stats:', error);
res.status(500).json({ error: 'Failed to get model stats', message: error.message });
}
});
// 🔧 系统管理
// 清理过期数据
router.post('/cleanup', authenticateAdmin, async (req, res) => {
try {
const [expiredKeys, errorAccounts] = await Promise.all([
apiKeyService.cleanupExpiredKeys(),
claudeAccountService.cleanupErrorAccounts()
]);
await redis.cleanup();
logger.success(`🧹 Admin triggered cleanup: ${expiredKeys} expired keys, ${errorAccounts} error accounts`);
res.json({
success: true,
message: 'Cleanup completed',
data: {
expiredKeysRemoved: expiredKeys,
errorAccountsReset: errorAccounts
}
});
} catch (error) {
logger.error('❌ Cleanup failed:', error);
res.status(500).json({ error: 'Cleanup failed', message: error.message });
}
});
// 获取使用趋势数据
router.get('/usage-trend', authenticateAdmin, async (req, res) => {
try {
const { days = 7 } = req.query;
const daysCount = parseInt(days) || 7;
const client = redis.getClientSafe();
const trendData = [];
const today = new Date();
// 获取过去N天的数据
for (let i = 0; i < daysCount; i++) {
const date = new Date(today);
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split('T')[0];
// 汇总当天所有API Key的使用数据
const pattern = `usage:daily:*:${dateStr}`;
const keys = await client.keys(pattern);
let dayInputTokens = 0;
let dayOutputTokens = 0;
let dayRequests = 0;
let dayCacheCreateTokens = 0;
let dayCacheReadTokens = 0;
let dayCost = 0;
for (const key of keys) {
const data = await client.hgetall(key);
if (data) {
dayInputTokens += parseInt(data.inputTokens) || 0;
dayOutputTokens += parseInt(data.outputTokens) || 0;
dayRequests += parseInt(data.requests) || 0;
dayCacheCreateTokens += parseInt(data.cacheCreateTokens) || 0;
dayCacheReadTokens += parseInt(data.cacheReadTokens) || 0;
}
}
// 计算当天费用(使用通用模型价格估算)
const usage = {
input_tokens: dayInputTokens,
output_tokens: dayOutputTokens,
cache_creation_input_tokens: dayCacheCreateTokens,
cache_read_input_tokens: dayCacheReadTokens
};
const costResult = CostCalculator.calculateCost(usage, 'unknown');
dayCost = costResult.costs.total;
trendData.push({
date: dateStr,
inputTokens: dayInputTokens,
outputTokens: dayOutputTokens,
requests: dayRequests,
cacheCreateTokens: dayCacheCreateTokens,
cacheReadTokens: dayCacheReadTokens,
totalTokens: dayInputTokens + dayOutputTokens + dayCacheCreateTokens + dayCacheReadTokens,
cost: dayCost,
formattedCost: CostCalculator.formatCost(dayCost)
});
}
// 按日期正序排列
trendData.sort((a, b) => new Date(a.date) - new Date(b.date));
res.json({ success: true, data: trendData });
} catch (error) {
logger.error('❌ Failed to get usage trend:', error);
res.status(500).json({ error: 'Failed to get usage trend', message: error.message });
}
});
// 获取单个API Key的模型统计
router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) => {
try {
const { keyId } = req.params;
const { period = 'monthly', startDate, endDate } = req.query;
logger.info(`📊 Getting model stats for API key: ${keyId}, period: ${period}, startDate: ${startDate}, endDate: ${endDate}`);
const client = redis.getClientSafe();
const today = new Date().toISOString().split('T')[0];
const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
let searchPatterns = [];
if (period === 'custom' && startDate && endDate) {
// 自定义日期范围,生成多个日期的搜索模式
const start = new Date(startDate);
const end = new Date(endDate);
// 确保日期范围有效
if (start > end) {
return res.status(400).json({ error: 'Start date must be before or equal to end date' });
}
// 限制最大范围为31天
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
if (daysDiff > 31) {
return res.status(400).json({ error: 'Date range cannot exceed 31 days' });
}
// 生成日期范围内所有日期的搜索模式
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const dateStr = d.toISOString().split('T')[0];
searchPatterns.push(`usage:${keyId}:model:daily:*:${dateStr}`);
}
logger.info(`📊 Custom date range patterns: ${searchPatterns.length} days from ${startDate} to ${endDate}`);
} else {
// 原有的预设期间逻辑
const pattern = period === 'daily' ?
`usage:${keyId}:model:daily:*:${today}` :
`usage:${keyId}:model:monthly:*:${currentMonth}`;
searchPatterns = [pattern];
logger.info(`📊 Preset period pattern: ${pattern}`);
}
// 汇总所有匹配的数据
const modelStatsMap = new Map();
const modelStats = []; // 定义结果数组
for (const pattern of searchPatterns) {
const keys = await client.keys(pattern);
logger.info(`📊 Pattern ${pattern} found ${keys.length} keys`);
for (const key of keys) {
const match = key.match(/usage:.+:model:daily:(.+):\d{4}-\d{2}-\d{2}$/) ||
key.match(/usage:.+:model:monthly:(.+):\d{4}-\d{2}$/);
if (!match) {
logger.warn(`📊 Pattern mismatch for key: ${key}`);
continue;
}
const model = match[1];
const data = await client.hgetall(key);
if (data && Object.keys(data).length > 0) {
// 累加同一模型的数据
if (!modelStatsMap.has(model)) {
modelStatsMap.set(model, {
requests: 0,
inputTokens: 0,
outputTokens: 0,
cacheCreateTokens: 0,
cacheReadTokens: 0,
allTokens: 0
});
}
const stats = modelStatsMap.get(model);
stats.requests += parseInt(data.requests) || 0;
stats.inputTokens += parseInt(data.inputTokens) || 0;
stats.outputTokens += parseInt(data.outputTokens) || 0;
stats.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0;
stats.cacheReadTokens += parseInt(data.cacheReadTokens) || 0;
stats.allTokens += parseInt(data.allTokens) || 0;
}
}
}
// 将汇总的数据转换为最终结果
for (const [model, stats] of modelStatsMap) {
logger.info(`📊 Model ${model} aggregated data:`, stats);
const usage = {
input_tokens: stats.inputTokens,
output_tokens: stats.outputTokens,
cache_creation_input_tokens: stats.cacheCreateTokens,
cache_read_input_tokens: stats.cacheReadTokens
};
// 使用CostCalculator计算费用
const costData = CostCalculator.calculateCost(usage, model);
modelStats.push({
model,
requests: stats.requests,
inputTokens: stats.inputTokens,
outputTokens: stats.outputTokens,
cacheCreateTokens: stats.cacheCreateTokens,
cacheReadTokens: stats.cacheReadTokens,
allTokens: stats.allTokens,
// 添加费用信息
costs: costData.costs,
formatted: costData.formatted,
pricing: costData.pricing,
usingDynamicPricing: costData.usingDynamicPricing
});
}
// 如果没有找到模型级别的详细数据,尝试从汇总数据中生成展示
if (modelStats.length === 0) {
logger.info(`📊 No detailed model stats found, trying to get aggregate data for API key ${keyId}`);
// 尝试从API Keys列表中获取usage数据作为备选方案
try {
const apiKeys = await apiKeyService.getAllApiKeys();
const targetApiKey = apiKeys.find(key => key.id === keyId);
if (targetApiKey && targetApiKey.usage) {
logger.info(`📊 Found API key usage data from getAllApiKeys for ${keyId}:`, targetApiKey.usage);
// 从汇总数据创建展示条目
let usageData;
if (period === 'custom' || period === 'daily') {
// 对于自定义或日统计使用daily数据或total数据
usageData = targetApiKey.usage.daily || targetApiKey.usage.total;
} else {
// 对于月统计使用monthly数据或total数据
usageData = targetApiKey.usage.monthly || targetApiKey.usage.total;
}
if (usageData && usageData.allTokens > 0) {
const usage = {
input_tokens: usageData.inputTokens || 0,
output_tokens: usageData.outputTokens || 0,
cache_creation_input_tokens: usageData.cacheCreateTokens || 0,
cache_read_input_tokens: usageData.cacheReadTokens || 0
};
// 对于汇总数据,使用默认模型计算费用
const costData = CostCalculator.calculateCost(usage, 'claude-3-5-sonnet-20241022');
modelStats.push({
model: '总体使用 (历史数据)',
requests: usageData.requests || 0,
inputTokens: usageData.inputTokens || 0,
outputTokens: usageData.outputTokens || 0,
cacheCreateTokens: usageData.cacheCreateTokens || 0,
cacheReadTokens: usageData.cacheReadTokens || 0,
allTokens: usageData.allTokens || 0,
// 添加费用信息
costs: costData.costs,
formatted: costData.formatted,
pricing: costData.pricing,
usingDynamicPricing: costData.usingDynamicPricing
});
logger.info('📊 Generated display data from API key usage stats');
} else {
logger.info(`📊 No usage data found for period ${period} in API key data`);
}
} else {
logger.info(`📊 API key ${keyId} not found or has no usage data`);
}
} catch (error) {
logger.error('❌ Error fetching API key usage data:', error);
}
}
// 按总token数降序排列
modelStats.sort((a, b) => b.allTokens - a.allTokens);
logger.info(`📊 Returning ${modelStats.length} model stats for API key ${keyId}:`, modelStats);
res.json({ success: true, data: modelStats });
} catch (error) {
logger.error('❌ Failed to get API key model stats:', error);
res.status(500).json({ error: 'Failed to get API key model stats', message: error.message });
}
});
// 计算总体使用费用
router.get('/usage-costs', authenticateAdmin, async (req, res) => {
try {
const { period = 'all' } = req.query; // all, today, monthly
logger.info(`💰 Calculating usage costs for period: ${period}`);
// 获取所有API Keys的使用统计
const apiKeys = await apiKeyService.getAllApiKeys();
let totalCosts = {
inputCost: 0,
outputCost: 0,
cacheCreateCost: 0,
cacheReadCost: 0,
totalCost: 0
};
let modelCosts = {};
// 按模型统计费用
const client = redis.getClientSafe();
const today = new Date().toISOString().split('T')[0];
const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
let pattern;
if (period === 'today') {
pattern = `usage:model:daily:*:${today}`;
} else if (period === 'monthly') {
pattern = `usage:model:monthly:*:${currentMonth}`;
} else {
// 全部时间使用API Key汇总数据
for (const apiKey of apiKeys) {
if (apiKey.usage && apiKey.usage.total) {
const usage = {
input_tokens: apiKey.usage.total.inputTokens || 0,
output_tokens: apiKey.usage.total.outputTokens || 0,
cache_creation_input_tokens: apiKey.usage.total.cacheCreateTokens || 0,
cache_read_input_tokens: apiKey.usage.total.cacheReadTokens || 0
};
// 计算未知模型的费用(汇总数据)
const costResult = CostCalculator.calculateCost(usage, 'unknown');
totalCosts.inputCost += costResult.costs.input;
totalCosts.outputCost += costResult.costs.output;
totalCosts.cacheCreateCost += costResult.costs.cacheWrite;
totalCosts.cacheReadCost += costResult.costs.cacheRead;
totalCosts.totalCost += costResult.costs.total;
}
}
res.json({
success: true,
data: {
period,
totalCosts: {
...totalCosts,
formatted: {
inputCost: CostCalculator.formatCost(totalCosts.inputCost),
outputCost: CostCalculator.formatCost(totalCosts.outputCost),
cacheCreateCost: CostCalculator.formatCost(totalCosts.cacheCreateCost),
cacheReadCost: CostCalculator.formatCost(totalCosts.cacheReadCost),
totalCost: CostCalculator.formatCost(totalCosts.totalCost)
}
},
modelCosts: [],
pricingServiceStatus: pricingService.getStatus()
}
});
return;
}
// 对于今日或本月从Redis获取详细的模型统计
const keys = await client.keys(pattern);
for (const key of keys) {
const match = key.match(period === 'today' ?
/usage:model:daily:(.+):\d{4}-\d{2}-\d{2}$/ :
/usage:model:monthly:(.+):\d{4}-\d{2}$/
);
if (!match) continue;
const model = match[1];
const data = await client.hgetall(key);
if (data && Object.keys(data).length > 0) {
const usage = {
input_tokens: parseInt(data.inputTokens) || 0,
output_tokens: parseInt(data.outputTokens) || 0,
cache_creation_input_tokens: parseInt(data.cacheCreateTokens) || 0,
cache_read_input_tokens: parseInt(data.cacheReadTokens) || 0
};
const costResult = CostCalculator.calculateCost(usage, model);
// 累加总费用
totalCosts.inputCost += costResult.costs.input;
totalCosts.outputCost += costResult.costs.output;
totalCosts.cacheCreateCost += costResult.costs.cacheWrite;
totalCosts.cacheReadCost += costResult.costs.cacheRead;
totalCosts.totalCost += costResult.costs.total;
// 记录模型费用
modelCosts[model] = {
model,
requests: parseInt(data.requests) || 0,
usage,
costs: costResult.costs,
formatted: costResult.formatted,
usingDynamicPricing: costResult.usingDynamicPricing
};
}
}
res.json({
success: true,
data: {
period,
totalCosts: {
...totalCosts,
formatted: {
inputCost: CostCalculator.formatCost(totalCosts.inputCost),
outputCost: CostCalculator.formatCost(totalCosts.outputCost),
cacheCreateCost: CostCalculator.formatCost(totalCosts.cacheCreateCost),
cacheReadCost: CostCalculator.formatCost(totalCosts.cacheReadCost),
totalCost: CostCalculator.formatCost(totalCosts.totalCost)
}
},
modelCosts: Object.values(modelCosts).sort((a, b) => b.costs.total - a.costs.total),
pricingServiceStatus: pricingService.getStatus()
}
});
} catch (error) {
logger.error('❌ Failed to calculate usage costs:', error);
res.status(500).json({ error: 'Failed to calculate usage costs', message: error.message });
}
});
module.exports = router;

225
src/routes/api.js Normal file
View File

@@ -0,0 +1,225 @@
const express = require('express');
const claudeRelayService = require('../services/claudeRelayService');
const apiKeyService = require('../services/apiKeyService');
const { authenticateApiKey } = require('../middleware/auth');
const logger = require('../utils/logger');
const router = express.Router();
// 🚀 Claude API messages 端点
router.post('/v1/messages', authenticateApiKey, async (req, res) => {
try {
const startTime = Date.now();
// 严格的输入验证
if (!req.body || typeof req.body !== 'object') {
return res.status(400).json({
error: 'Invalid request',
message: 'Request body must be a valid JSON object'
});
}
if (!req.body.messages || !Array.isArray(req.body.messages)) {
return res.status(400).json({
error: 'Invalid request',
message: 'Missing or invalid field: messages (must be an array)'
});
}
if (req.body.messages.length === 0) {
return res.status(400).json({
error: 'Invalid request',
message: 'Messages array cannot be empty'
});
}
// 检查是否为流式请求
const isStream = req.body.stream === true;
logger.api(`🚀 Processing ${isStream ? 'stream' : 'non-stream'} request for key: ${req.apiKey.name}`);
if (isStream) {
// 流式响应 - 只使用官方真实usage数据
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('Access-Control-Allow-Origin', '*');
let usageDataCaptured = false;
// 使用自定义流处理器来捕获usage数据
await claudeRelayService.relayStreamRequestWithUsageCapture(req.body, req.apiKey, res, (usageData) => {
// 回调函数当检测到完整usage数据时记录真实token使用量
logger.info('🎯 Usage callback triggered with complete data:', JSON.stringify(usageData, null, 2));
if (usageData && usageData.input_tokens !== undefined && usageData.output_tokens !== undefined) {
const inputTokens = usageData.input_tokens || 0;
const outputTokens = usageData.output_tokens || 0;
const cacheCreateTokens = usageData.cache_creation_input_tokens || 0;
const cacheReadTokens = usageData.cache_read_input_tokens || 0;
const model = usageData.model || 'unknown';
// 记录真实的token使用量包含模型信息和所有4种token
apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model).catch(error => {
logger.error('❌ Failed to record stream usage:', error);
});
usageDataCaptured = true;
logger.api(`📊 Stream usage recorded (real) - Model: ${model}, Input: ${inputTokens}, Output: ${outputTokens}, Cache Create: ${cacheCreateTokens}, Cache Read: ${cacheReadTokens}, Total: ${inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens} tokens`);
} else {
logger.warn('⚠️ Usage callback triggered but data is incomplete:', JSON.stringify(usageData));
}
});
// 流式请求完成后 - 如果没有捕获到usage数据记录警告但不进行估算
setTimeout(() => {
if (!usageDataCaptured) {
logger.warn('⚠️ No usage data captured from SSE stream - no statistics recorded (official data only)');
}
}, 1000); // 1秒后检查
} else {
// 非流式响应 - 只使用官方真实usage数据
logger.info('📄 Starting non-streaming request', {
apiKeyId: req.apiKey.id,
apiKeyName: req.apiKey.name
});
const response = await claudeRelayService.relayRequest(req.body, req.apiKey);
logger.info('📡 Claude API response received', {
statusCode: response.statusCode,
headers: JSON.stringify(response.headers),
bodyLength: response.body ? response.body.length : 0
});
res.status(response.statusCode);
// 设置响应头
Object.keys(response.headers).forEach(key => {
if (key.toLowerCase() !== 'content-encoding') {
res.setHeader(key, response.headers[key]);
}
});
let usageRecorded = false;
// 尝试解析JSON响应并提取usage信息
try {
const jsonData = JSON.parse(response.body);
logger.info('📊 Parsed Claude API response:', JSON.stringify(jsonData, null, 2));
// 从Claude API响应中提取usage信息完整的token分类体系
if (jsonData.usage && jsonData.usage.input_tokens !== undefined && jsonData.usage.output_tokens !== undefined) {
const inputTokens = jsonData.usage.input_tokens || 0;
const outputTokens = jsonData.usage.output_tokens || 0;
const cacheCreateTokens = jsonData.usage.cache_creation_input_tokens || 0;
const cacheReadTokens = jsonData.usage.cache_read_input_tokens || 0;
const model = jsonData.model || req.body.model || 'unknown';
// 记录真实的token使用量包含模型信息和所有4种token
await apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model);
usageRecorded = true;
logger.api(`📊 Non-stream usage recorded (real) - Model: ${model}, Input: ${inputTokens}, Output: ${outputTokens}, Cache Create: ${cacheCreateTokens}, Cache Read: ${cacheReadTokens}, Total: ${inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens} tokens`);
} else {
logger.warn('⚠️ No usage data found in Claude API JSON response');
}
res.json(jsonData);
} catch (parseError) {
logger.warn('⚠️ Failed to parse Claude API response as JSON:', parseError.message);
logger.info('📄 Raw response body:', response.body);
res.send(response.body);
}
// 如果没有记录usage只记录警告不进行估算
if (!usageRecorded) {
logger.warn('⚠️ No usage data recorded for non-stream request - no statistics recorded (official data only)');
}
}
const duration = Date.now() - startTime;
logger.api(`✅ Request completed in ${duration}ms for key: ${req.apiKey.name}`);
} catch (error) {
logger.error('❌ Claude relay error:', error);
if (!res.headersSent) {
res.status(500).json({
error: 'Relay service error',
message: error.message
});
}
}
});
// 🏥 健康检查端点
router.get('/health', async (req, res) => {
try {
const healthStatus = await claudeRelayService.healthCheck();
res.status(healthStatus.healthy ? 200 : 503).json({
status: healthStatus.healthy ? 'healthy' : 'unhealthy',
service: 'claude-relay-service',
version: '1.0.0',
...healthStatus
});
} catch (error) {
logger.error('❌ Health check error:', error);
res.status(503).json({
status: 'unhealthy',
service: 'claude-relay-service',
error: error.message,
timestamp: new Date().toISOString()
});
}
});
// 📊 API Key状态检查端点
router.get('/v1/key-info', authenticateApiKey, async (req, res) => {
try {
const usage = await apiKeyService.getUsageStats(req.apiKey.id);
res.json({
keyInfo: {
id: req.apiKey.id,
name: req.apiKey.name,
tokenLimit: req.apiKey.tokenLimit,
requestLimit: req.apiKey.requestLimit,
usage
},
timestamp: new Date().toISOString()
});
} catch (error) {
logger.error('❌ Key info error:', error);
res.status(500).json({
error: 'Failed to get key info',
message: error.message
});
}
});
// 📈 使用统计端点
router.get('/v1/usage', authenticateApiKey, async (req, res) => {
try {
const usage = await apiKeyService.getUsageStats(req.apiKey.id);
res.json({
usage,
limits: {
tokens: req.apiKey.tokenLimit,
requests: req.apiKey.requestLimit
},
timestamp: new Date().toISOString()
});
} catch (error) {
logger.error('❌ Usage stats error:', error);
res.status(500).json({
error: 'Failed to get usage stats',
message: error.message
});
}
});
module.exports = router;

202
src/routes/web.js Normal file
View File

@@ -0,0 +1,202 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
const path = require('path');
const fs = require('fs');
const redis = require('../models/redis');
const logger = require('../utils/logger');
const config = require('../../config/config');
const router = express.Router();
// 🏠 服务静态文件
router.use('/assets', express.static(path.join(__dirname, '../../web/assets')));
// 🔒 Web管理界面文件白名单 - 仅允许这些特定文件
const ALLOWED_FILES = {
'index.html': {
path: path.join(__dirname, '../../web/admin/index.html'),
contentType: 'text/html; charset=utf-8'
},
'app.js': {
path: path.join(__dirname, '../../web/admin/app.js'),
contentType: 'application/javascript; charset=utf-8'
},
'style.css': {
path: path.join(__dirname, '../../web/admin/style.css'),
contentType: 'text/css; charset=utf-8'
}
};
// 🛡️ 安全文件服务函数
function serveWhitelistedFile(req, res, filename) {
const fileConfig = ALLOWED_FILES[filename];
if (!fileConfig) {
logger.security(`🚨 Attempted access to non-whitelisted file: ${filename}`);
return res.status(404).json({ error: 'File not found' });
}
try {
// 检查文件是否存在
if (!fs.existsSync(fileConfig.path)) {
logger.error(`❌ Whitelisted file not found: ${fileConfig.path}`);
return res.status(404).json({ error: 'File not found' });
}
// 读取并返回文件内容
const content = fs.readFileSync(fileConfig.path, 'utf8');
res.setHeader('Content-Type', fileConfig.contentType);
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.send(content);
logger.info(`📄 Served whitelisted file: ${filename}`);
} catch (error) {
logger.error(`❌ Error serving file ${filename}:`, error);
res.status(500).json({ error: 'Internal server error' });
}
}
// 🔐 管理员登录
router.post('/auth/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({
error: 'Missing credentials',
message: 'Username and password are required'
});
}
// 从Redis获取管理员信息
const adminData = await redis.getSession('admin_credentials');
if (!adminData || Object.keys(adminData).length === 0) {
return res.status(401).json({
error: 'Invalid credentials',
message: 'Invalid username or password'
});
}
// 验证用户名和密码
const isValidUsername = adminData.username === username;
const isValidPassword = await bcrypt.compare(password, adminData.passwordHash);
if (!isValidUsername || !isValidPassword) {
logger.security(`🔒 Failed login attempt for username: ${username}`);
return res.status(401).json({
error: 'Invalid credentials',
message: 'Invalid username or password'
});
}
// 生成会话token
const sessionId = crypto.randomBytes(32).toString('hex');
// 存储会话
const sessionData = {
username: adminData.username,
loginTime: new Date().toISOString(),
lastActivity: new Date().toISOString()
};
await redis.setSession(sessionId, sessionData, config.security.adminSessionTimeout);
// 更新最后登录时间
adminData.lastLogin = new Date().toISOString();
await redis.setSession('admin_credentials', adminData);
logger.success(`🔐 Admin login successful: ${username}`);
res.json({
success: true,
token: sessionId,
expiresIn: config.security.adminSessionTimeout
});
} catch (error) {
logger.error('❌ Login error:', error);
res.status(500).json({
error: 'Login failed',
message: 'Internal server error'
});
}
});
// 🚪 管理员登出
router.post('/auth/logout', async (req, res) => {
try {
const token = req.headers['authorization']?.replace('Bearer ', '') || req.cookies?.adminToken;
if (token) {
await redis.deleteSession(token);
logger.success('🚪 Admin logout successful');
}
res.json({ success: true, message: 'Logout successful' });
} catch (error) {
logger.error('❌ Logout error:', error);
res.status(500).json({
error: 'Logout failed',
message: 'Internal server error'
});
}
});
// 🔄 刷新token
router.post('/auth/refresh', async (req, res) => {
try {
const token = req.headers['authorization']?.replace('Bearer ', '') || req.cookies?.adminToken;
if (!token) {
return res.status(401).json({
error: 'No token provided',
message: 'Authentication required'
});
}
const sessionData = await redis.getSession(token);
if (!sessionData) {
return res.status(401).json({
error: 'Invalid token',
message: 'Session expired or invalid'
});
}
// 更新最后活动时间
sessionData.lastActivity = new Date().toISOString();
await redis.setSession(token, sessionData, config.security.adminSessionTimeout);
res.json({
success: true,
token: token,
expiresIn: config.security.adminSessionTimeout
});
} catch (error) {
logger.error('❌ Token refresh error:', error);
res.status(500).json({
error: 'Token refresh failed',
message: 'Internal server error'
});
}
});
// 🌐 Web管理界面路由 - 使用固定白名单
router.get('/', (req, res) => {
serveWhitelistedFile(req, res, 'index.html');
});
router.get('/app.js', (req, res) => {
serveWhitelistedFile(req, res, 'app.js');
});
router.get('/style.css', (req, res) => {
serveWhitelistedFile(req, res, 'style.css');
});
module.exports = router;