feat: 添加API Key时间窗口限流功能并移除累计总量限制

- 新增时间窗口限流功能,支持按分钟设置时间窗口
- 支持在时间窗口内限制请求次数和Token使用量
- 移除原有的累计总量限制,只保留时间窗口限制
- Token统计包含所有4种类型:输入、输出、缓存创建、缓存读取
- 前端UI优化,明确显示限流参数的作用范围
- 限流触发时提供友好的错误提示和重置时间

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-07-20 15:58:00 +08:00
parent 0aa986a0d8
commit 088ce266ba
7 changed files with 224 additions and 16 deletions

View File

@@ -19,6 +19,8 @@ class ApiKeyService {
claudeAccountId = null,
isActive = true,
concurrencyLimit = 0,
rateLimitWindow = null,
rateLimitRequests = null,
enableModelRestriction = false,
restrictedModels = []
} = options;
@@ -35,6 +37,8 @@ class ApiKeyService {
apiKey: hashedKey,
tokenLimit: String(tokenLimit ?? 0),
concurrencyLimit: String(concurrencyLimit ?? 0),
rateLimitWindow: String(rateLimitWindow ?? 0),
rateLimitRequests: String(rateLimitRequests ?? 0),
isActive: String(isActive),
claudeAccountId: claudeAccountId || '',
enableModelRestriction: String(enableModelRestriction),
@@ -57,6 +61,8 @@ class ApiKeyService {
description: keyData.description,
tokenLimit: parseInt(keyData.tokenLimit),
concurrencyLimit: parseInt(keyData.concurrencyLimit),
rateLimitWindow: parseInt(keyData.rateLimitWindow || 0),
rateLimitRequests: parseInt(keyData.rateLimitRequests || 0),
isActive: keyData.isActive === 'true',
claudeAccountId: keyData.claudeAccountId,
enableModelRestriction: keyData.enableModelRestriction === 'true',
@@ -94,14 +100,8 @@ class ApiKeyService {
return { valid: false, error: 'API key has expired' };
}
// 检查使用限制
// 获取使用统计(供返回数据使用)
const usage = await redis.getUsageStats(keyData.id);
const tokenLimit = parseInt(keyData.tokenLimit);
if (tokenLimit > 0 && usage.total.tokens >= tokenLimit) {
return { valid: false, error: 'Token limit exceeded' };
}
// 更新最后使用时间优化只在实际API调用时更新而不是验证时
// 注意lastUsedAt的更新已移至recordUsage方法中
@@ -124,6 +124,8 @@ class ApiKeyService {
claudeAccountId: keyData.claudeAccountId,
tokenLimit: parseInt(keyData.tokenLimit),
concurrencyLimit: parseInt(keyData.concurrencyLimit || 0),
rateLimitWindow: parseInt(keyData.rateLimitWindow || 0),
rateLimitRequests: parseInt(keyData.rateLimitRequests || 0),
enableModelRestriction: keyData.enableModelRestriction === 'true',
restrictedModels: restrictedModels,
usage
@@ -145,6 +147,8 @@ class ApiKeyService {
key.usage = await redis.getUsageStats(key.id);
key.tokenLimit = parseInt(key.tokenLimit);
key.concurrencyLimit = parseInt(key.concurrencyLimit || 0);
key.rateLimitWindow = parseInt(key.rateLimitWindow || 0);
key.rateLimitRequests = parseInt(key.rateLimitRequests || 0);
key.currentConcurrency = await redis.getConcurrency(key.id);
key.isActive = key.isActive === 'true';
key.enableModelRestriction = key.enableModelRestriction === 'true';
@@ -172,7 +176,7 @@ class ApiKeyService {
}
// 允许更新的字段
const allowedUpdates = ['name', 'description', 'tokenLimit', 'concurrencyLimit', 'isActive', 'claudeAccountId', 'expiresAt', 'enableModelRestriction', 'restrictedModels'];
const allowedUpdates = ['name', 'description', 'tokenLimit', 'concurrencyLimit', 'rateLimitWindow', 'rateLimitRequests', 'isActive', 'claudeAccountId', 'expiresAt', 'enableModelRestriction', 'restrictedModels'];
const updatedData = { ...keyData };
for (const [field, value] of Object.entries(updates)) {

View File

@@ -23,7 +23,7 @@ class ClaudeRelayService {
try {
// 调试日志查看API Key数据
logger.info(`🔍 API Key data received:`, {
logger.info('🔍 API Key data received:', {
apiKeyName: apiKeyData.name,
enableModelRestriction: apiKeyData.enableModelRestriction,
restrictedModels: apiKeyData.restrictedModels,
@@ -448,7 +448,7 @@ class ClaudeRelayService {
async relayStreamRequestWithUsageCapture(requestBody, apiKeyData, responseStream, clientHeaders, usageCallback) {
try {
// 调试日志查看API Key数据流式请求
logger.info(`🔍 [Stream] API Key data received:`, {
logger.info('🔍 [Stream] API Key data received:', {
apiKeyName: apiKeyData.name,
enableModelRestriction: apiKeyData.enableModelRestriction,
restrictedModels: apiKeyData.restrictedModels,