mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
fix: 1.修复ClaudeConsole账号设置为专属绑定的功能
2. 修复Claude 官方账号会话窗口计算错误的问题
This commit is contained in:
@@ -340,6 +340,7 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
tokenLimit,
|
||||
expiresAt,
|
||||
claudeAccountId,
|
||||
claudeConsoleAccountId,
|
||||
geminiAccountId,
|
||||
permissions,
|
||||
concurrencyLimit,
|
||||
@@ -416,6 +417,7 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
tokenLimit,
|
||||
expiresAt,
|
||||
claudeAccountId,
|
||||
claudeConsoleAccountId,
|
||||
geminiAccountId,
|
||||
permissions,
|
||||
concurrencyLimit,
|
||||
@@ -441,7 +443,7 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { keyId } = req.params;
|
||||
const { tokenLimit, concurrencyLimit, rateLimitWindow, rateLimitRequests, claudeAccountId, geminiAccountId, permissions, enableModelRestriction, restrictedModels, enableClientRestriction, allowedClients, expiresAt, dailyCostLimit, tags } = req.body;
|
||||
const { tokenLimit, concurrencyLimit, rateLimitWindow, rateLimitRequests, claudeAccountId, claudeConsoleAccountId, geminiAccountId, permissions, enableModelRestriction, restrictedModels, enableClientRestriction, allowedClients, expiresAt, dailyCostLimit, tags } = req.body;
|
||||
|
||||
// 只允许更新指定字段
|
||||
const updates = {};
|
||||
@@ -478,6 +480,11 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
// 空字符串表示解绑,null或空字符串都设置为空字符串
|
||||
updates.claudeAccountId = claudeAccountId || '';
|
||||
}
|
||||
|
||||
if (claudeConsoleAccountId !== undefined) {
|
||||
// 空字符串表示解绑,null或空字符串都设置为空字符串
|
||||
updates.claudeConsoleAccountId = claudeConsoleAccountId || '';
|
||||
}
|
||||
|
||||
if (geminiAccountId !== undefined) {
|
||||
// 空字符串表示解绑,null或空字符串都设置为空字符串
|
||||
|
||||
@@ -17,6 +17,7 @@ class ApiKeyService {
|
||||
tokenLimit = config.limits.defaultTokenLimit,
|
||||
expiresAt = null,
|
||||
claudeAccountId = null,
|
||||
claudeConsoleAccountId = null,
|
||||
geminiAccountId = null,
|
||||
permissions = 'all', // 'claude', 'gemini', 'all'
|
||||
isActive = true,
|
||||
@@ -47,6 +48,7 @@ class ApiKeyService {
|
||||
rateLimitRequests: String(rateLimitRequests ?? 0),
|
||||
isActive: String(isActive),
|
||||
claudeAccountId: claudeAccountId || '',
|
||||
claudeConsoleAccountId: claudeConsoleAccountId || '',
|
||||
geminiAccountId: geminiAccountId || '',
|
||||
permissions: permissions || 'all',
|
||||
enableModelRestriction: String(enableModelRestriction),
|
||||
@@ -77,6 +79,7 @@ class ApiKeyService {
|
||||
rateLimitRequests: parseInt(keyData.rateLimitRequests || 0),
|
||||
isActive: keyData.isActive === 'true',
|
||||
claudeAccountId: keyData.claudeAccountId,
|
||||
claudeConsoleAccountId: keyData.claudeConsoleAccountId,
|
||||
geminiAccountId: keyData.geminiAccountId,
|
||||
permissions: keyData.permissions,
|
||||
enableModelRestriction: keyData.enableModelRestriction === 'true',
|
||||
@@ -162,6 +165,7 @@ class ApiKeyService {
|
||||
createdAt: keyData.createdAt,
|
||||
expiresAt: keyData.expiresAt,
|
||||
claudeAccountId: keyData.claudeAccountId,
|
||||
claudeConsoleAccountId: keyData.claudeConsoleAccountId,
|
||||
geminiAccountId: keyData.geminiAccountId,
|
||||
permissions: keyData.permissions || 'all',
|
||||
tokenLimit: parseInt(keyData.tokenLimit),
|
||||
@@ -237,7 +241,7 @@ class ApiKeyService {
|
||||
}
|
||||
|
||||
// 允许更新的字段
|
||||
const allowedUpdates = ['name', 'description', 'tokenLimit', 'concurrencyLimit', 'rateLimitWindow', 'rateLimitRequests', 'isActive', 'claudeAccountId', 'geminiAccountId', 'permissions', 'expiresAt', 'enableModelRestriction', 'restrictedModels', 'enableClientRestriction', 'allowedClients', 'dailyCostLimit', 'tags'];
|
||||
const allowedUpdates = ['name', 'description', 'tokenLimit', 'concurrencyLimit', 'rateLimitWindow', 'rateLimitRequests', 'isActive', 'claudeAccountId', 'claudeConsoleAccountId', 'geminiAccountId', 'permissions', 'expiresAt', 'enableModelRestriction', 'restrictedModels', 'enableClientRestriction', 'allowedClients', 'dailyCostLimit', 'tags'];
|
||||
const updatedData = { ...keyData };
|
||||
|
||||
for (const [field, value] of Object.entries(updates)) {
|
||||
|
||||
@@ -892,7 +892,10 @@ class ClaudeAccountService {
|
||||
const windowStartHour = Math.floor(hour / 5) * 5; // 向下取整到最近的5小时边界
|
||||
|
||||
const windowStart = new Date(requestTime);
|
||||
windowStart.setHours(windowStartHour, 0, 0, 0);
|
||||
windowStart.setHours(windowStartHour);
|
||||
windowStart.setMinutes(0);
|
||||
windowStart.setSeconds(0);
|
||||
windowStart.setMilliseconds(0);
|
||||
|
||||
return windowStart;
|
||||
}
|
||||
@@ -969,79 +972,67 @@ class ClaudeAccountService {
|
||||
logger.info('🔄 Initializing session windows for all Claude accounts...');
|
||||
|
||||
const accounts = await redis.getAllClaudeAccounts();
|
||||
let initializedCount = 0;
|
||||
let skippedCount = 0;
|
||||
let expiredCount = 0;
|
||||
let validWindowCount = 0;
|
||||
let expiredWindowCount = 0;
|
||||
let noWindowCount = 0;
|
||||
const now = new Date();
|
||||
|
||||
for (const account of accounts) {
|
||||
// 如果已经有会话窗口信息且不强制重算,跳过
|
||||
if (account.sessionWindowStart && account.sessionWindowEnd && !forceRecalculate) {
|
||||
skippedCount++;
|
||||
logger.debug(`⏭️ Skipped account ${account.name} (${account.id}) - already has session window`);
|
||||
continue;
|
||||
// 如果强制重算,清除现有窗口信息
|
||||
if (forceRecalculate && (account.sessionWindowStart || account.sessionWindowEnd)) {
|
||||
logger.info(`🔄 Force recalculating window for account ${account.name} (${account.id})`);
|
||||
delete account.sessionWindowStart;
|
||||
delete account.sessionWindowEnd;
|
||||
delete account.lastRequestTime;
|
||||
await redis.setClaudeAccount(account.id, account);
|
||||
}
|
||||
|
||||
// 如果有lastUsedAt,基于它恢复会话窗口
|
||||
if (account.lastUsedAt) {
|
||||
const lastUsedTime = new Date(account.lastUsedAt);
|
||||
const now = new Date();
|
||||
// 检查现有会话窗口
|
||||
if (account.sessionWindowStart && account.sessionWindowEnd) {
|
||||
const windowEnd = new Date(account.sessionWindowEnd);
|
||||
const windowStart = new Date(account.sessionWindowStart);
|
||||
const timeUntilExpires = Math.round((windowEnd.getTime() - now.getTime()) / (1000 * 60));
|
||||
|
||||
// 计算时间差(分钟)
|
||||
const timeSinceLastUsed = Math.round((now.getTime() - lastUsedTime.getTime()) / (1000 * 60));
|
||||
|
||||
// 计算lastUsedAt对应的会话窗口
|
||||
const windowStart = this._calculateSessionWindowStart(lastUsedTime);
|
||||
const windowEnd = this._calculateSessionWindowEnd(windowStart);
|
||||
|
||||
// 计算窗口剩余时间(分钟)
|
||||
const timeUntilWindowExpires = Math.round((windowEnd.getTime() - now.getTime()) / (1000 * 60));
|
||||
|
||||
logger.info(`🔍 Analyzing account ${account.name} (${account.id}):`);
|
||||
logger.info(` Last used: ${lastUsedTime.toISOString()} (${timeSinceLastUsed} minutes ago)`);
|
||||
logger.info(` Calculated window: ${windowStart.toISOString()} - ${windowEnd.toISOString()}`);
|
||||
logger.info(` Window expires in: ${timeUntilWindowExpires > 0 ? timeUntilWindowExpires + ' minutes' : 'EXPIRED'}`);
|
||||
|
||||
// 只有窗口未过期才恢复
|
||||
if (now.getTime() < windowEnd.getTime()) {
|
||||
account.sessionWindowStart = windowStart.toISOString();
|
||||
account.sessionWindowEnd = windowEnd.toISOString();
|
||||
account.lastRequestTime = account.lastUsedAt;
|
||||
|
||||
await redis.setClaudeAccount(account.id, account);
|
||||
initializedCount++;
|
||||
|
||||
logger.success(`✅ Initialized session window for account ${account.name} (${account.id})`);
|
||||
// 窗口仍然有效,保留它
|
||||
validWindowCount++;
|
||||
logger.info(`✅ Account ${account.name} (${account.id}) has valid window: ${windowStart.toISOString()} - ${windowEnd.toISOString()} (${timeUntilExpires} minutes remaining)`);
|
||||
} else {
|
||||
expiredCount++;
|
||||
logger.warn(`⏰ Window expired for account ${account.name} (${account.id}) - will create new window on next request`);
|
||||
// 窗口已过期,清除它
|
||||
expiredWindowCount++;
|
||||
logger.warn(`⏰ Account ${account.name} (${account.id}) window expired: ${windowStart.toISOString()} - ${windowEnd.toISOString()}`);
|
||||
|
||||
// 清除过期的窗口信息
|
||||
delete account.sessionWindowStart;
|
||||
delete account.sessionWindowEnd;
|
||||
delete account.lastRequestTime;
|
||||
await redis.setClaudeAccount(account.id, account);
|
||||
}
|
||||
} else {
|
||||
logger.info(`📭 No lastUsedAt data for account ${account.name} (${account.id}) - will create window on first request`);
|
||||
noWindowCount++;
|
||||
logger.info(`📭 Account ${account.name} (${account.id}) has no session window - will create on next request`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.success('✅ Session window initialization completed:');
|
||||
logger.success(` 📊 Total accounts: ${accounts.length}`);
|
||||
logger.success(` ✅ Initialized: ${initializedCount}`);
|
||||
logger.success(` ⏭️ Skipped (existing): ${skippedCount}`);
|
||||
logger.success(` ⏰ Expired: ${expiredCount}`);
|
||||
logger.success(` 📭 No usage data: ${accounts.length - initializedCount - skippedCount - expiredCount}`);
|
||||
logger.success(` ✅ Valid windows: ${validWindowCount}`);
|
||||
logger.success(` ⏰ Expired windows: ${expiredWindowCount}`);
|
||||
logger.success(` 📭 No windows: ${noWindowCount}`);
|
||||
|
||||
return {
|
||||
total: accounts.length,
|
||||
initialized: initializedCount,
|
||||
skipped: skippedCount,
|
||||
expired: expiredCount,
|
||||
noData: accounts.length - initializedCount - skippedCount - expiredCount
|
||||
validWindows: validWindowCount,
|
||||
expiredWindows: expiredWindowCount,
|
||||
noWindows: noWindowCount
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to initialize session windows:', error);
|
||||
return {
|
||||
total: 0,
|
||||
initialized: 0,
|
||||
skipped: 0,
|
||||
expired: 0,
|
||||
noData: 0,
|
||||
validWindows: 0,
|
||||
expiredWindows: 0,
|
||||
noWindows: 0,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,16 +12,31 @@ class UnifiedClaudeScheduler {
|
||||
async selectAccountForApiKey(apiKeyData, sessionHash = null, requestedModel = null) {
|
||||
try {
|
||||
// 如果API Key绑定了专属账户,优先使用
|
||||
// 1. 检查Claude OAuth账户绑定
|
||||
if (apiKeyData.claudeAccountId) {
|
||||
const boundAccount = await redis.getClaudeAccount(apiKeyData.claudeAccountId);
|
||||
if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') {
|
||||
logger.info(`🎯 Using bound dedicated Claude account: ${boundAccount.name} (${apiKeyData.claudeAccountId}) for API key ${apiKeyData.name}`);
|
||||
logger.info(`🎯 Using bound dedicated Claude OAuth account: ${boundAccount.name} (${apiKeyData.claudeAccountId}) for API key ${apiKeyData.name}`);
|
||||
return {
|
||||
accountId: apiKeyData.claudeAccountId,
|
||||
accountType: 'claude-official'
|
||||
};
|
||||
} else {
|
||||
logger.warn(`⚠️ Bound Claude account ${apiKeyData.claudeAccountId} is not available, falling back to pool`);
|
||||
logger.warn(`⚠️ Bound Claude OAuth account ${apiKeyData.claudeAccountId} is not available, falling back to pool`);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查Claude Console账户绑定
|
||||
if (apiKeyData.claudeConsoleAccountId) {
|
||||
const boundConsoleAccount = await claudeConsoleAccountService.getAccount(apiKeyData.claudeConsoleAccountId);
|
||||
if (boundConsoleAccount && boundConsoleAccount.isActive === true && boundConsoleAccount.status === 'active') {
|
||||
logger.info(`🎯 Using bound dedicated Claude Console account: ${boundConsoleAccount.name} (${apiKeyData.claudeConsoleAccountId}) for API key ${apiKeyData.name}`);
|
||||
return {
|
||||
accountId: apiKeyData.claudeConsoleAccountId,
|
||||
accountType: 'claude-console'
|
||||
};
|
||||
} else {
|
||||
logger.warn(`⚠️ Bound Claude Console account ${apiKeyData.claudeConsoleAccountId} is not available, falling back to pool`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,13 +96,14 @@ class UnifiedClaudeScheduler {
|
||||
async _getAllAvailableAccounts(apiKeyData, requestedModel = null) {
|
||||
const availableAccounts = [];
|
||||
|
||||
// 如果API Key绑定了专属Claude账户,优先返回
|
||||
// 如果API Key绑定了专属账户,优先返回
|
||||
// 1. 检查Claude OAuth账户绑定
|
||||
if (apiKeyData.claudeAccountId) {
|
||||
const boundAccount = await redis.getClaudeAccount(apiKeyData.claudeAccountId);
|
||||
if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error' && boundAccount.status !== 'blocked') {
|
||||
const isRateLimited = await claudeAccountService.isAccountRateLimited(boundAccount.id);
|
||||
if (!isRateLimited) {
|
||||
logger.info(`🎯 Using bound dedicated Claude account: ${boundAccount.name} (${apiKeyData.claudeAccountId})`);
|
||||
logger.info(`🎯 Using bound dedicated Claude OAuth account: ${boundAccount.name} (${apiKeyData.claudeAccountId})`);
|
||||
return [{
|
||||
...boundAccount,
|
||||
accountId: boundAccount.id,
|
||||
@@ -97,7 +113,27 @@ class UnifiedClaudeScheduler {
|
||||
}];
|
||||
}
|
||||
} else {
|
||||
logger.warn(`⚠️ Bound Claude account ${apiKeyData.claudeAccountId} is not available`);
|
||||
logger.warn(`⚠️ Bound Claude OAuth account ${apiKeyData.claudeAccountId} is not available`);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查Claude Console账户绑定
|
||||
if (apiKeyData.claudeConsoleAccountId) {
|
||||
const boundConsoleAccount = await claudeConsoleAccountService.getAccount(apiKeyData.claudeConsoleAccountId);
|
||||
if (boundConsoleAccount && boundConsoleAccount.isActive === true && boundConsoleAccount.status === 'active') {
|
||||
const isRateLimited = await claudeConsoleAccountService.isAccountRateLimited(boundConsoleAccount.id);
|
||||
if (!isRateLimited) {
|
||||
logger.info(`🎯 Using bound dedicated Claude Console account: ${boundConsoleAccount.name} (${apiKeyData.claudeConsoleAccountId})`);
|
||||
return [{
|
||||
...boundConsoleAccount,
|
||||
accountId: boundConsoleAccount.id,
|
||||
accountType: 'claude-console',
|
||||
priority: parseInt(boundConsoleAccount.priority) || 50,
|
||||
lastUsedAt: boundConsoleAccount.lastUsedAt || '0'
|
||||
}];
|
||||
}
|
||||
} else {
|
||||
logger.warn(`⚠️ Bound Claude Console account ${apiKeyData.claudeConsoleAccountId} is not available`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user