mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
合并 main 分支到 dev 分支
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
const express = require('express');
|
||||
const apiKeyService = require('../services/apiKeyService');
|
||||
const claudeAccountService = require('../services/claudeAccountService');
|
||||
const claudeConsoleAccountService = require('../services/claudeConsoleAccountService');
|
||||
const geminiAccountService = require('../services/geminiAccountService');
|
||||
const redis = require('../models/redis');
|
||||
const { authenticateAdmin } = require('../middleware/auth');
|
||||
@@ -703,7 +704,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
|
||||
refreshToken,
|
||||
claudeAiOauth,
|
||||
proxy,
|
||||
accountType
|
||||
accountType,
|
||||
priority
|
||||
} = req.body;
|
||||
|
||||
if (!name) {
|
||||
@@ -715,6 +717,11 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
|
||||
return res.status(400).json({ error: 'Invalid account type. Must be "shared" or "dedicated"' });
|
||||
}
|
||||
|
||||
// 验证priority的有效性
|
||||
if (priority !== undefined && (typeof priority !== 'number' || priority < 1 || priority > 100)) {
|
||||
return res.status(400).json({ error: 'Priority must be a number between 1 and 100' });
|
||||
}
|
||||
|
||||
const newAccount = await claudeAccountService.createAccount({
|
||||
name,
|
||||
description,
|
||||
@@ -723,7 +730,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
|
||||
refreshToken,
|
||||
claudeAiOauth,
|
||||
proxy,
|
||||
accountType: accountType || 'shared' // 默认为共享类型
|
||||
accountType: accountType || 'shared', // 默认为共享类型
|
||||
priority: priority || 50 // 默认优先级为50
|
||||
});
|
||||
|
||||
logger.success(`🏢 Admin created new Claude account: ${name} (${accountType || 'shared'})`);
|
||||
@@ -740,6 +748,11 @@ router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) =>
|
||||
const { accountId } = req.params;
|
||||
const updates = req.body;
|
||||
|
||||
// 验证priority的有效性
|
||||
if (updates.priority !== undefined && (typeof updates.priority !== 'number' || updates.priority < 1 || updates.priority > 100)) {
|
||||
return res.status(400).json({ error: 'Priority must be a number between 1 and 100' });
|
||||
}
|
||||
|
||||
await claudeAccountService.updateAccount(accountId, updates);
|
||||
|
||||
logger.success(`📝 Admin updated Claude account: ${accountId}`);
|
||||
@@ -780,6 +793,198 @@ router.post('/claude-accounts/:accountId/refresh', authenticateAdmin, async (req
|
||||
}
|
||||
});
|
||||
|
||||
// 切换Claude账户调度状态
|
||||
router.put('/claude-accounts/:accountId/toggle-schedulable', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params;
|
||||
|
||||
const accounts = await claudeAccountService.getAllAccounts();
|
||||
const account = accounts.find(acc => acc.id === accountId);
|
||||
|
||||
if (!account) {
|
||||
return res.status(404).json({ error: 'Account not found' });
|
||||
}
|
||||
|
||||
const newSchedulable = !account.schedulable;
|
||||
await claudeAccountService.updateAccount(accountId, { schedulable: newSchedulable });
|
||||
|
||||
logger.success(`🔄 Admin toggled Claude account schedulable status: ${accountId} -> ${newSchedulable ? 'schedulable' : 'not schedulable'}`);
|
||||
res.json({ success: true, schedulable: newSchedulable });
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to toggle Claude account schedulable status:', error);
|
||||
res.status(500).json({ error: 'Failed to toggle schedulable status', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 🎮 Claude Console 账户管理
|
||||
|
||||
// 获取所有Claude Console账户
|
||||
router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const accounts = await claudeConsoleAccountService.getAllAccounts();
|
||||
|
||||
// 为每个账户添加使用统计信息
|
||||
const accountsWithStats = await Promise.all(accounts.map(async (account) => {
|
||||
try {
|
||||
const usageStats = await redis.getAccountUsageStats(account.id);
|
||||
return {
|
||||
...account,
|
||||
usage: {
|
||||
daily: usageStats.daily,
|
||||
total: usageStats.total,
|
||||
averages: usageStats.averages
|
||||
}
|
||||
};
|
||||
} catch (statsError) {
|
||||
logger.warn(`⚠️ Failed to get usage stats for Claude Console account ${account.id}:`, statsError.message);
|
||||
return {
|
||||
...account,
|
||||
usage: {
|
||||
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
||||
total: { tokens: 0, requests: 0, allTokens: 0 },
|
||||
averages: { rpm: 0, tpm: 0 }
|
||||
}
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
res.json({ success: true, data: accountsWithStats });
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to get Claude Console accounts:', error);
|
||||
res.status(500).json({ error: 'Failed to get Claude Console accounts', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 创建新的Claude Console账户
|
||||
router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
apiUrl,
|
||||
apiKey,
|
||||
priority,
|
||||
supportedModels,
|
||||
userAgent,
|
||||
rateLimitDuration,
|
||||
proxy,
|
||||
accountType
|
||||
} = req.body;
|
||||
|
||||
if (!name || !apiUrl || !apiKey) {
|
||||
return res.status(400).json({ error: 'Name, API URL and API Key are required' });
|
||||
}
|
||||
|
||||
// 验证priority的有效性(1-100)
|
||||
if (priority !== undefined && (priority < 1 || priority > 100)) {
|
||||
return res.status(400).json({ error: 'Priority must be between 1 and 100' });
|
||||
}
|
||||
|
||||
// 验证accountType的有效性
|
||||
if (accountType && !['shared', 'dedicated'].includes(accountType)) {
|
||||
return res.status(400).json({ error: 'Invalid account type. Must be "shared" or "dedicated"' });
|
||||
}
|
||||
|
||||
const newAccount = await claudeConsoleAccountService.createAccount({
|
||||
name,
|
||||
description,
|
||||
apiUrl,
|
||||
apiKey,
|
||||
priority: priority || 50,
|
||||
supportedModels: supportedModels || [],
|
||||
userAgent,
|
||||
rateLimitDuration: rateLimitDuration || 60,
|
||||
proxy,
|
||||
accountType: accountType || 'shared'
|
||||
});
|
||||
|
||||
logger.success(`🎮 Admin created Claude Console account: ${name}`);
|
||||
res.json({ success: true, data: newAccount });
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to create Claude Console account:', error);
|
||||
res.status(500).json({ error: 'Failed to create Claude Console account', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 更新Claude Console账户
|
||||
router.put('/claude-console-accounts/:accountId', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params;
|
||||
const updates = req.body;
|
||||
|
||||
// 验证priority的有效性(1-100)
|
||||
if (updates.priority !== undefined && (updates.priority < 1 || updates.priority > 100)) {
|
||||
return res.status(400).json({ error: 'Priority must be between 1 and 100' });
|
||||
}
|
||||
|
||||
await claudeConsoleAccountService.updateAccount(accountId, updates);
|
||||
|
||||
logger.success(`📝 Admin updated Claude Console account: ${accountId}`);
|
||||
res.json({ success: true, message: 'Claude Console account updated successfully' });
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to update Claude Console account:', error);
|
||||
res.status(500).json({ error: 'Failed to update Claude Console account', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除Claude Console账户
|
||||
router.delete('/claude-console-accounts/:accountId', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params;
|
||||
|
||||
await claudeConsoleAccountService.deleteAccount(accountId);
|
||||
|
||||
logger.success(`🗑️ Admin deleted Claude Console account: ${accountId}`);
|
||||
res.json({ success: true, message: 'Claude Console account deleted successfully' });
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to delete Claude Console account:', error);
|
||||
res.status(500).json({ error: 'Failed to delete Claude Console account', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 切换Claude Console账户状态
|
||||
router.put('/claude-console-accounts/:accountId/toggle', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params;
|
||||
|
||||
const account = await claudeConsoleAccountService.getAccount(accountId);
|
||||
if (!account) {
|
||||
return res.status(404).json({ error: 'Account not found' });
|
||||
}
|
||||
|
||||
const newStatus = !account.isActive;
|
||||
await claudeConsoleAccountService.updateAccount(accountId, { isActive: newStatus });
|
||||
|
||||
logger.success(`🔄 Admin toggled Claude Console account status: ${accountId} -> ${newStatus ? 'active' : 'inactive'}`);
|
||||
res.json({ success: true, isActive: newStatus });
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to toggle Claude Console account status:', error);
|
||||
res.status(500).json({ error: 'Failed to toggle account status', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 切换Claude Console账户调度状态
|
||||
router.put('/claude-console-accounts/:accountId/toggle-schedulable', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params;
|
||||
|
||||
const account = await claudeConsoleAccountService.getAccount(accountId);
|
||||
if (!account) {
|
||||
return res.status(404).json({ error: 'Account not found' });
|
||||
}
|
||||
|
||||
const newSchedulable = !account.schedulable;
|
||||
await claudeConsoleAccountService.updateAccount(accountId, { schedulable: newSchedulable });
|
||||
|
||||
logger.success(`🔄 Admin toggled Claude Console account schedulable status: ${accountId} -> ${newSchedulable ? 'schedulable' : 'not schedulable'}`);
|
||||
res.json({ success: true, schedulable: newSchedulable });
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to toggle Claude Console account schedulable status:', error);
|
||||
res.status(500).json({ error: 'Failed to toggle schedulable status', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 🤖 Gemini 账户管理
|
||||
|
||||
// 生成 Gemini OAuth 授权 URL
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
const express = require('express');
|
||||
const claudeRelayService = require('../services/claudeRelayService');
|
||||
const claudeConsoleRelayService = require('../services/claudeConsoleRelayService');
|
||||
const unifiedClaudeScheduler = require('../services/unifiedClaudeScheduler');
|
||||
const apiKeyService = require('../services/apiKeyService');
|
||||
const { authenticateApiKey } = require('../middleware/auth');
|
||||
const logger = require('../utils/logger');
|
||||
const redis = require('../models/redis');
|
||||
const sessionHelper = require('../utils/sessionHelper');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -56,8 +59,17 @@ async function handleMessagesRequest(req, res) {
|
||||
|
||||
let usageDataCaptured = false;
|
||||
|
||||
// 使用自定义流处理器来捕获usage数据
|
||||
await claudeRelayService.relayStreamRequestWithUsageCapture(req.body, req.apiKey, res, req.headers, (usageData) => {
|
||||
// 生成会话哈希用于sticky会话
|
||||
const sessionHash = sessionHelper.generateSessionHash(req.body);
|
||||
|
||||
// 使用统一调度选择账号(传递请求的模型)
|
||||
const requestedModel = req.body.model;
|
||||
const { accountId, accountType } = await unifiedClaudeScheduler.selectAccountForApiKey(req.apiKey, sessionHash, requestedModel);
|
||||
|
||||
// 根据账号类型选择对应的转发服务并调用
|
||||
if (accountType === 'claude-official') {
|
||||
// 官方Claude账号使用原有的转发服务(会自己选择账号)
|
||||
await claudeRelayService.relayStreamRequestWithUsageCapture(req.body, req.apiKey, res, req.headers, (usageData) => {
|
||||
// 回调函数:当检测到完整usage数据时记录真实token使用量
|
||||
logger.info('🎯 Usage callback triggered with complete data:', JSON.stringify(usageData, null, 2));
|
||||
|
||||
@@ -88,7 +100,42 @@ async function handleMessagesRequest(req, res) {
|
||||
} else {
|
||||
logger.warn('⚠️ Usage callback triggered but data is incomplete:', JSON.stringify(usageData));
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Claude Console账号使用Console转发服务(需要传递accountId)
|
||||
await claudeConsoleRelayService.relayStreamRequestWithUsageCapture(req.body, req.apiKey, res, req.headers, (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以及账户ID)
|
||||
const usageAccountId = usageData.accountId;
|
||||
apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model, usageAccountId).catch(error => {
|
||||
logger.error('❌ Failed to record stream usage:', error);
|
||||
});
|
||||
|
||||
// 更新时间窗口内的token计数
|
||||
if (req.rateLimitInfo) {
|
||||
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
|
||||
redis.getClient().incrby(req.rateLimitInfo.tokenCountKey, totalTokens).catch(error => {
|
||||
logger.error('❌ Failed to update rate limit token count:', error);
|
||||
});
|
||||
logger.api(`📊 Updated rate limit token count: +${totalTokens} tokens`);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}, accountId);
|
||||
}
|
||||
|
||||
// 流式请求完成后 - 如果没有捕获到usage数据,记录警告但不进行估算
|
||||
setTimeout(() => {
|
||||
@@ -103,7 +150,27 @@ async function handleMessagesRequest(req, res) {
|
||||
apiKeyName: req.apiKey.name
|
||||
});
|
||||
|
||||
const response = await claudeRelayService.relayRequest(req.body, req.apiKey, req, res, req.headers);
|
||||
// 生成会话哈希用于sticky会话
|
||||
const sessionHash = sessionHelper.generateSessionHash(req.body);
|
||||
|
||||
// 使用统一调度选择账号(传递请求的模型)
|
||||
const requestedModel = req.body.model;
|
||||
const { accountId, accountType } = await unifiedClaudeScheduler.selectAccountForApiKey(req.apiKey, sessionHash, requestedModel);
|
||||
|
||||
// 根据账号类型选择对应的转发服务
|
||||
let response;
|
||||
logger.debug(`[DEBUG] Request query params: ${JSON.stringify(req.query)}`);
|
||||
logger.debug(`[DEBUG] Request URL: ${req.url}`);
|
||||
logger.debug(`[DEBUG] Request path: ${req.path}`);
|
||||
|
||||
if (accountType === 'claude-official') {
|
||||
// 官方Claude账号使用原有的转发服务
|
||||
response = await claudeRelayService.relayRequest(req.body, req.apiKey, req, res, req.headers);
|
||||
} else {
|
||||
// Claude Console账号使用Console转发服务
|
||||
logger.debug(`[DEBUG] Calling claudeConsoleRelayService.relayRequest with accountId: ${accountId}`);
|
||||
response = await claudeConsoleRelayService.relayRequest(req.body, req.apiKey, req, res, req.headers, accountId);
|
||||
}
|
||||
|
||||
logger.info('📡 Claude API response received', {
|
||||
statusCode: response.statusCode,
|
||||
|
||||
Reference in New Issue
Block a user