feat: 为API Key添加模型限制功能

- 前端:在API Key创建和编辑表单中添加模型限制开关和标签输入
- 前端:支持动态添加/删除限制的模型列表
- 后端:更新API Key数据结构,新增enableModelRestriction和restrictedModels字段
- 后端:在中转请求时检查模型访问权限
- 修复:Enter键提交表单问题,使用@keydown.enter.prevent
- 优化:限制模型数据持久化,关闭开关时不清空数据

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-07-19 20:54:26 +08:00
parent f9933f7061
commit f962083752
9 changed files with 278 additions and 22 deletions

View File

@@ -18,7 +18,9 @@ class ApiKeyService {
expiresAt = null,
claudeAccountId = null,
isActive = true,
concurrencyLimit = 0
concurrencyLimit = 0,
enableModelRestriction = false,
restrictedModels = []
} = options;
// 生成简单的API Key (64字符十六进制)
@@ -35,6 +37,8 @@ class ApiKeyService {
concurrencyLimit: String(concurrencyLimit ?? 0),
isActive: String(isActive),
claudeAccountId: claudeAccountId || '',
enableModelRestriction: String(enableModelRestriction),
restrictedModels: JSON.stringify(restrictedModels || []),
createdAt: new Date().toISOString(),
lastUsedAt: '',
expiresAt: expiresAt || '',
@@ -55,6 +59,8 @@ class ApiKeyService {
concurrencyLimit: parseInt(keyData.concurrencyLimit),
isActive: keyData.isActive === 'true',
claudeAccountId: keyData.claudeAccountId,
enableModelRestriction: keyData.enableModelRestriction === 'true',
restrictedModels: JSON.parse(keyData.restrictedModels),
createdAt: keyData.createdAt,
expiresAt: keyData.expiresAt,
createdBy: keyData.createdBy
@@ -131,6 +137,12 @@ class ApiKeyService {
key.concurrencyLimit = parseInt(key.concurrencyLimit || 0);
key.currentConcurrency = await redis.getConcurrency(key.id);
key.isActive = key.isActive === 'true';
key.enableModelRestriction = key.enableModelRestriction === 'true';
try {
key.restrictedModels = key.restrictedModels ? JSON.parse(key.restrictedModels) : [];
} catch (e) {
key.restrictedModels = [];
}
delete key.apiKey; // 不返回哈希后的key
}
@@ -150,12 +162,20 @@ class ApiKeyService {
}
// 允许更新的字段
const allowedUpdates = ['name', 'description', 'tokenLimit', 'concurrencyLimit', 'isActive', 'claudeAccountId', 'expiresAt'];
const allowedUpdates = ['name', 'description', 'tokenLimit', 'concurrencyLimit', 'isActive', 'claudeAccountId', 'expiresAt', 'enableModelRestriction', 'restrictedModels'];
const updatedData = { ...keyData };
for (const [field, value] of Object.entries(updates)) {
if (allowedUpdates.includes(field)) {
updatedData[field] = (value != null ? value : '').toString();
if (field === 'restrictedModels') {
// 特殊处理 restrictedModels 数组
updatedData[field] = JSON.stringify(value || []);
} else if (field === 'enableModelRestriction') {
// 布尔值转字符串
updatedData[field] = String(value);
} else {
updatedData[field] = (value != null ? value : '').toString();
}
}
}