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

@@ -117,7 +117,10 @@ const app = createApp({
tokenLimit: '',
description: '',
concurrencyLimit: '',
claudeAccountId: ''
claudeAccountId: '',
enableModelRestriction: false,
restrictedModels: [],
modelInput: ''
},
apiKeyModelStats: {}, // 存储每个key的模型统计数据
expandedApiKeys: {}, // 跟踪展开的API Keys
@@ -155,7 +158,10 @@ const app = createApp({
name: '',
tokenLimit: '',
concurrencyLimit: '',
claudeAccountId: ''
claudeAccountId: '',
enableModelRestriction: false,
restrictedModels: [],
modelInput: ''
},
// 账户
@@ -313,6 +319,20 @@ const app = createApp({
return this.apiKeys.filter(key => key.claudeAccountId === accountId).length;
},
// 添加限制模型
addRestrictedModel(form) {
const model = form.modelInput.trim();
if (model && !form.restrictedModels.includes(model)) {
form.restrictedModels.push(model);
form.modelInput = '';
}
},
// 移除限制模型
removeRestrictedModel(form, index) {
form.restrictedModels.splice(index, 1);
},
// Toast 通知方法
showToast(message, type = 'info', title = null, duration = 5000) {
const id = ++this.toastIdCounter;
@@ -1191,7 +1211,9 @@ const app = createApp({
tokenLimit: this.apiKeyForm.tokenLimit && this.apiKeyForm.tokenLimit.trim() ? parseInt(this.apiKeyForm.tokenLimit) : null,
description: this.apiKeyForm.description || '',
concurrencyLimit: this.apiKeyForm.concurrencyLimit && this.apiKeyForm.concurrencyLimit.trim() ? parseInt(this.apiKeyForm.concurrencyLimit) : 0,
claudeAccountId: this.apiKeyForm.claudeAccountId || null
claudeAccountId: this.apiKeyForm.claudeAccountId || null,
enableModelRestriction: this.apiKeyForm.enableModelRestriction,
restrictedModels: this.apiKeyForm.restrictedModels
})
});
@@ -1209,7 +1231,7 @@ const app = createApp({
// 关闭创建弹窗并清理表单
this.showCreateApiKeyModal = false;
this.apiKeyForm = { name: '', tokenLimit: '', description: '', concurrencyLimit: '', claudeAccountId: '' };
this.apiKeyForm = { name: '', tokenLimit: '', description: '', concurrencyLimit: '', claudeAccountId: '', enableModelRestriction: false, restrictedModels: [], modelInput: '' };
// 重新加载API Keys列表
await this.loadApiKeys();
@@ -1253,7 +1275,10 @@ const app = createApp({
name: key.name,
tokenLimit: key.tokenLimit || '',
concurrencyLimit: key.concurrencyLimit || '',
claudeAccountId: key.claudeAccountId || ''
claudeAccountId: key.claudeAccountId || '',
enableModelRestriction: key.enableModelRestriction || false,
restrictedModels: key.restrictedModels ? [...key.restrictedModels] : [],
modelInput: ''
};
this.showEditApiKeyModal = true;
},
@@ -1265,7 +1290,10 @@ const app = createApp({
name: '',
tokenLimit: '',
concurrencyLimit: '',
claudeAccountId: ''
claudeAccountId: '',
enableModelRestriction: false,
restrictedModels: [],
modelInput: ''
};
},
@@ -1281,7 +1309,9 @@ const app = createApp({
body: JSON.stringify({
tokenLimit: this.editApiKeyForm.tokenLimit && this.editApiKeyForm.tokenLimit.toString().trim() !== '' ? parseInt(this.editApiKeyForm.tokenLimit) : 0,
concurrencyLimit: this.editApiKeyForm.concurrencyLimit && this.editApiKeyForm.concurrencyLimit.toString().trim() !== '' ? parseInt(this.editApiKeyForm.concurrencyLimit) : 0,
claudeAccountId: this.editApiKeyForm.claudeAccountId || null
claudeAccountId: this.editApiKeyForm.claudeAccountId || null,
enableModelRestriction: this.editApiKeyForm.enableModelRestriction,
restrictedModels: this.editApiKeyForm.restrictedModels
})
});

View File

@@ -1842,6 +1842,62 @@
<p class="text-xs text-gray-500 mt-2">选择专属账号后此API Key将只使用该账号不选择则使用共享账号池</p>
</div>
<div>
<div class="flex items-center mb-3">
<input
type="checkbox"
v-model="apiKeyForm.enableModelRestriction"
id="enableModelRestriction"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
>
<label for="enableModelRestriction" class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer">
启用模型限制
</label>
</div>
<div v-if="apiKeyForm.enableModelRestriction" class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-600 mb-2">限制的模型列表</label>
<div class="flex flex-wrap gap-2 mb-3 min-h-[32px] p-2 bg-gray-50 rounded-lg border border-gray-200">
<span
v-for="(model, index) in apiKeyForm.restrictedModels"
:key="index"
class="inline-flex items-center px-3 py-1 rounded-full text-sm bg-red-100 text-red-800"
>
{{ model }}
<button
type="button"
@click="removeRestrictedModel(apiKeyForm, index)"
class="ml-2 text-red-600 hover:text-red-800"
>
<i class="fas fa-times text-xs"></i>
</button>
</span>
<span v-if="apiKeyForm.restrictedModels.length === 0" class="text-gray-400 text-sm">
暂无限制的模型
</span>
</div>
<div class="flex gap-2">
<input
v-model="apiKeyForm.modelInput"
@keydown.enter.prevent="addRestrictedModel(apiKeyForm)"
type="text"
placeholder="输入模型名称,按回车添加"
class="form-input flex-1"
>
<button
type="button"
@click="addRestrictedModel(apiKeyForm)"
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
>
<i class="fas fa-plus"></i>
</button>
</div>
<p class="text-xs text-gray-500 mt-2">设置此API Key无法访问的模型例如claude-opus-4-20250514</p>
</div>
</div>
</div>
<div class="flex gap-3 pt-4">
<button
type="button"
@@ -1936,6 +1992,62 @@
<p class="text-xs text-gray-500 mt-2">修改绑定账号将影响此API Key的请求路由</p>
</div>
<div>
<div class="flex items-center mb-3">
<input
type="checkbox"
v-model="editApiKeyForm.enableModelRestriction"
id="editEnableModelRestriction"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
>
<label for="editEnableModelRestriction" class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer">
启用模型限制
</label>
</div>
<div v-if="editApiKeyForm.enableModelRestriction" class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-600 mb-2">限制的模型列表</label>
<div class="flex flex-wrap gap-2 mb-3 min-h-[32px] p-2 bg-gray-50 rounded-lg border border-gray-200">
<span
v-for="(model, index) in editApiKeyForm.restrictedModels"
:key="index"
class="inline-flex items-center px-3 py-1 rounded-full text-sm bg-red-100 text-red-800"
>
{{ model }}
<button
type="button"
@click="removeRestrictedModel(editApiKeyForm, index)"
class="ml-2 text-red-600 hover:text-red-800"
>
<i class="fas fa-times text-xs"></i>
</button>
</span>
<span v-if="editApiKeyForm.restrictedModels.length === 0" class="text-gray-400 text-sm">
暂无限制的模型
</span>
</div>
<div class="flex gap-2">
<input
v-model="editApiKeyForm.modelInput"
@keydown.enter.prevent="addRestrictedModel(editApiKeyForm)"
type="text"
placeholder="输入模型名称,按回车添加"
class="form-input flex-1"
>
<button
type="button"
@click="addRestrictedModel(editApiKeyForm)"
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
>
<i class="fas fa-plus"></i>
</button>
</div>
<p class="text-xs text-gray-500 mt-2">设置此API Key无法访问的模型例如claude-opus-4-20250514</p>
</div>
</div>
</div>
<div class="flex gap-3 pt-4">
<button
type="button"