mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 添加API Key并发控制和编辑功能
- 新增API Key并发控制功能 - 创建API Key时可设置并发限制(0为不限制) - 在认证中间件中实现并发检查 - 使用Redis原子操作确保计数准确 - 添加自动清理机制处理异常情况 - 新增API Key编辑功能 - 支持修改Token限制和并发限制 - 前端添加编辑按钮和模态框 - 后端限制只能修改指定字段 - 其他改进 - 添加test-concurrency.js测试脚本 - 添加详细的功能说明文档 - 所有代码通过ESLint检查 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -61,13 +61,46 @@ const authenticateApiKey = async (req, res, next) => {
|
||||
res.setHeader('X-RateLimit-Reset', rateLimitResult.resetTime);
|
||||
res.setHeader('X-RateLimit-Policy', `${rateLimitResult.limit};w=60`);
|
||||
|
||||
// 检查并发限制
|
||||
const concurrencyLimit = validation.keyData.concurrencyLimit || 0;
|
||||
if (concurrencyLimit > 0) {
|
||||
const currentConcurrency = await redis.incrConcurrency(validation.keyData.id);
|
||||
|
||||
if (currentConcurrency > concurrencyLimit) {
|
||||
// 如果超过限制,立即减少计数
|
||||
await redis.decrConcurrency(validation.keyData.id);
|
||||
logger.security(`🚦 Concurrency limit exceeded for key: ${validation.keyData.id} (${validation.keyData.name}), current: ${currentConcurrency - 1}, limit: ${concurrencyLimit}`);
|
||||
return res.status(429).json({
|
||||
error: 'Concurrency limit exceeded',
|
||||
message: `Too many concurrent requests. Limit: ${concurrencyLimit} concurrent requests`,
|
||||
currentConcurrency: currentConcurrency - 1,
|
||||
concurrencyLimit
|
||||
});
|
||||
}
|
||||
|
||||
// 在响应结束时减少并发计数
|
||||
res.on('finish', () => {
|
||||
redis.decrConcurrency(validation.keyData.id).catch(error => {
|
||||
logger.error('Failed to decrement concurrency:', error);
|
||||
});
|
||||
});
|
||||
|
||||
// 在响应错误时也减少并发计数
|
||||
res.on('error', () => {
|
||||
redis.decrConcurrency(validation.keyData.id).catch(error => {
|
||||
logger.error('Failed to decrement concurrency on error:', error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 将验证信息添加到请求对象(只包含必要信息)
|
||||
req.apiKey = {
|
||||
id: validation.keyData.id,
|
||||
name: validation.keyData.name,
|
||||
tokenLimit: validation.keyData.tokenLimit,
|
||||
requestLimit: validation.keyData.requestLimit,
|
||||
claudeAccountId: validation.keyData.claudeAccountId
|
||||
claudeAccountId: validation.keyData.claudeAccountId,
|
||||
concurrencyLimit: validation.keyData.concurrencyLimit
|
||||
};
|
||||
req.usage = validation.keyData.usage;
|
||||
|
||||
|
||||
@@ -714,6 +714,53 @@ class RedisClient {
|
||||
logger.error('❌ Redis cleanup failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 增加并发计数
|
||||
async incrConcurrency(apiKeyId) {
|
||||
try {
|
||||
const key = `concurrency:${apiKeyId}`;
|
||||
const count = await this.client.incr(key);
|
||||
|
||||
// 设置过期时间为5分钟,防止计数器永远不清零
|
||||
await this.client.expire(key, 300);
|
||||
|
||||
return count;
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to increment concurrency:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 减少并发计数
|
||||
async decrConcurrency(apiKeyId) {
|
||||
try {
|
||||
const key = `concurrency:${apiKeyId}`;
|
||||
const count = await this.client.decr(key);
|
||||
|
||||
// 如果计数降到0或以下,删除键
|
||||
if (count <= 0) {
|
||||
await this.client.del(key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return count;
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to decrement concurrency:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前并发数
|
||||
async getConcurrency(apiKeyId) {
|
||||
try {
|
||||
const key = `concurrency:${apiKeyId}`;
|
||||
const count = await this.client.get(key);
|
||||
return parseInt(count || 0);
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to get concurrency:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new RedisClient();
|
||||
@@ -32,7 +32,8 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
tokenLimit,
|
||||
requestLimit,
|
||||
expiresAt,
|
||||
claudeAccountId
|
||||
claudeAccountId,
|
||||
concurrencyLimit
|
||||
} = req.body;
|
||||
|
||||
// 输入验证
|
||||
@@ -56,13 +57,18 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
return res.status(400).json({ error: 'Request limit must be a non-negative integer' });
|
||||
}
|
||||
|
||||
if (concurrencyLimit !== undefined && concurrencyLimit !== null && concurrencyLimit !== '' && (!Number.isInteger(Number(concurrencyLimit)) || Number(concurrencyLimit) < 0)) {
|
||||
return res.status(400).json({ error: 'Concurrency limit must be a non-negative integer' });
|
||||
}
|
||||
|
||||
const newKey = await apiKeyService.generateApiKey({
|
||||
name,
|
||||
description,
|
||||
tokenLimit,
|
||||
requestLimit,
|
||||
expiresAt,
|
||||
claudeAccountId
|
||||
claudeAccountId,
|
||||
concurrencyLimit
|
||||
});
|
||||
|
||||
logger.success(`🔑 Admin created new API key: ${name}`);
|
||||
@@ -77,7 +83,24 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { keyId } = req.params;
|
||||
const updates = req.body;
|
||||
const { tokenLimit, concurrencyLimit } = req.body;
|
||||
|
||||
// 只允许更新tokenLimit和concurrencyLimit
|
||||
const updates = {};
|
||||
|
||||
if (tokenLimit !== undefined && tokenLimit !== null && tokenLimit !== '') {
|
||||
if (!Number.isInteger(Number(tokenLimit)) || Number(tokenLimit) < 0) {
|
||||
return res.status(400).json({ error: 'Token limit must be a non-negative integer' });
|
||||
}
|
||||
updates.tokenLimit = Number(tokenLimit);
|
||||
}
|
||||
|
||||
if (concurrencyLimit !== undefined && concurrencyLimit !== null && concurrencyLimit !== '') {
|
||||
if (!Number.isInteger(Number(concurrencyLimit)) || Number(concurrencyLimit) < 0) {
|
||||
return res.status(400).json({ error: 'Concurrency limit must be a non-negative integer' });
|
||||
}
|
||||
updates.concurrencyLimit = Number(concurrencyLimit);
|
||||
}
|
||||
|
||||
await apiKeyService.updateApiKey(keyId, updates);
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ class ApiKeyService {
|
||||
requestLimit = config.limits.defaultRequestLimit,
|
||||
expiresAt = null,
|
||||
claudeAccountId = null,
|
||||
isActive = true
|
||||
isActive = true,
|
||||
concurrencyLimit = 0
|
||||
} = options;
|
||||
|
||||
// 生成简单的API Key (64字符十六进制)
|
||||
@@ -33,6 +34,7 @@ class ApiKeyService {
|
||||
apiKey: hashedKey,
|
||||
tokenLimit: String(tokenLimit ?? 0),
|
||||
requestLimit: String(requestLimit ?? 0),
|
||||
concurrencyLimit: String(concurrencyLimit ?? 0),
|
||||
isActive: String(isActive),
|
||||
claudeAccountId: claudeAccountId || '',
|
||||
createdAt: new Date().toISOString(),
|
||||
@@ -53,6 +55,7 @@ class ApiKeyService {
|
||||
description: keyData.description,
|
||||
tokenLimit: parseInt(keyData.tokenLimit),
|
||||
requestLimit: parseInt(keyData.requestLimit),
|
||||
concurrencyLimit: parseInt(keyData.concurrencyLimit),
|
||||
isActive: keyData.isActive === 'true',
|
||||
claudeAccountId: keyData.claudeAccountId,
|
||||
createdAt: keyData.createdAt,
|
||||
@@ -114,6 +117,7 @@ class ApiKeyService {
|
||||
claudeAccountId: keyData.claudeAccountId,
|
||||
tokenLimit: parseInt(keyData.tokenLimit),
|
||||
requestLimit: parseInt(keyData.requestLimit),
|
||||
concurrencyLimit: parseInt(keyData.concurrencyLimit || 0),
|
||||
usage
|
||||
}
|
||||
};
|
||||
@@ -133,6 +137,7 @@ class ApiKeyService {
|
||||
key.usage = await redis.getUsageStats(key.id);
|
||||
key.tokenLimit = parseInt(key.tokenLimit);
|
||||
key.requestLimit = parseInt(key.requestLimit);
|
||||
key.concurrencyLimit = parseInt(key.concurrencyLimit || 0);
|
||||
key.isActive = key.isActive === 'true';
|
||||
delete key.apiKey; // 不返回哈希后的key
|
||||
}
|
||||
@@ -153,7 +158,7 @@ class ApiKeyService {
|
||||
}
|
||||
|
||||
// 允许更新的字段
|
||||
const allowedUpdates = ['name', 'description', 'tokenLimit', 'requestLimit', 'isActive', 'claudeAccountId', 'expiresAt'];
|
||||
const allowedUpdates = ['name', 'description', 'tokenLimit', 'requestLimit', 'concurrencyLimit', 'isActive', 'claudeAccountId', 'expiresAt'];
|
||||
const updatedData = { ...keyData };
|
||||
|
||||
for (const [field, value] of Object.entries(updates)) {
|
||||
|
||||
Reference in New Issue
Block a user