mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
Merge branch 'dev': 添加API Key模型限制功能
- 前端添加模型限制的开关和输入界面 - 后端存储和验证模型限制 - 修复模型限制不生效的问题
This commit is contained in:
@@ -43,6 +43,8 @@ LOG_MAX_FILES=5
|
||||
CLEANUP_INTERVAL=3600000
|
||||
TOKEN_USAGE_RETENTION=2592000000
|
||||
HEALTH_CHECK_INTERVAL=60000
|
||||
SYSTEM_TIMEZONE=Asia/Shanghai
|
||||
TIMEZONE_OFFSET=8
|
||||
|
||||
# 🎨 Web 界面配置
|
||||
WEB_TITLE=Claude Relay Service
|
||||
|
||||
@@ -62,7 +62,9 @@ const config = {
|
||||
system: {
|
||||
cleanupInterval: parseInt(process.env.CLEANUP_INTERVAL) || 3600000, // 1小时
|
||||
tokenUsageRetention: parseInt(process.env.TOKEN_USAGE_RETENTION) || 2592000000, // 30天
|
||||
healthCheckInterval: parseInt(process.env.HEALTH_CHECK_INTERVAL) || 60000 // 1分钟
|
||||
healthCheckInterval: parseInt(process.env.HEALTH_CHECK_INTERVAL) || 60000, // 1分钟
|
||||
timezone: process.env.SYSTEM_TIMEZONE || 'Asia/Shanghai', // 默认UTC+8(中国时区)
|
||||
timezoneOffset: parseInt(process.env.TIMEZONE_OFFSET) || 8 // UTC偏移小时数,默认+8
|
||||
},
|
||||
|
||||
// 🎨 Web界面配置
|
||||
|
||||
@@ -109,7 +109,9 @@ const authenticateApiKey = async (req, res, next) => {
|
||||
name: validation.keyData.name,
|
||||
tokenLimit: validation.keyData.tokenLimit,
|
||||
claudeAccountId: validation.keyData.claudeAccountId,
|
||||
concurrencyLimit: validation.keyData.concurrencyLimit
|
||||
concurrencyLimit: validation.keyData.concurrencyLimit,
|
||||
enableModelRestriction: validation.keyData.enableModelRestriction,
|
||||
restrictedModels: validation.keyData.restrictedModels
|
||||
};
|
||||
req.usage = validation.keyData.usage;
|
||||
|
||||
|
||||
@@ -2,6 +2,26 @@ const Redis = require('ioredis');
|
||||
const config = require('../../config/config');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// 时区辅助函数
|
||||
function getDateInTimezone(date = new Date()) {
|
||||
const offset = config.system.timezoneOffset || 8; // 默认UTC+8
|
||||
const utcTime = date.getTime() + (date.getTimezoneOffset() * 60000);
|
||||
const targetTime = new Date(utcTime + (offset * 3600000));
|
||||
return targetTime;
|
||||
}
|
||||
|
||||
// 获取配置时区的日期字符串 (YYYY-MM-DD)
|
||||
function getDateStringInTimezone(date = new Date()) {
|
||||
const tzDate = getDateInTimezone(date);
|
||||
return `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}-${String(tzDate.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// 获取配置时区的小时 (0-23)
|
||||
function getHourInTimezone(date = new Date()) {
|
||||
const tzDate = getDateInTimezone(date);
|
||||
return tzDate.getHours();
|
||||
}
|
||||
|
||||
class RedisClient {
|
||||
constructor() {
|
||||
this.client = null;
|
||||
@@ -140,9 +160,10 @@ class RedisClient {
|
||||
async incrementTokenUsage(keyId, tokens, inputTokens = 0, outputTokens = 0, cacheCreateTokens = 0, cacheReadTokens = 0, model = 'unknown') {
|
||||
const key = `usage:${keyId}`;
|
||||
const now = new Date();
|
||||
const today = now.toISOString().split('T')[0];
|
||||
const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
||||
const currentHour = `${today}:${String(now.getHours()).padStart(2, '0')}`; // 新增小时级别
|
||||
const today = getDateStringInTimezone(now);
|
||||
const tzDate = getDateInTimezone(now);
|
||||
const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`;
|
||||
const currentHour = `${today}:${String(getHourInTimezone(now)).padStart(2, '0')}`; // 新增小时级别
|
||||
|
||||
const daily = `usage:daily:${keyId}:${today}`;
|
||||
const monthly = `usage:monthly:${keyId}:${currentMonth}`;
|
||||
@@ -263,9 +284,10 @@ class RedisClient {
|
||||
|
||||
async getUsageStats(keyId) {
|
||||
const totalKey = `usage:${keyId}`;
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const today = getDateStringInTimezone();
|
||||
const dailyKey = `usage:daily:${keyId}:${today}`;
|
||||
const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
|
||||
const tzDate = getDateInTimezone();
|
||||
const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`;
|
||||
const monthlyKey = `usage:monthly:${keyId}:${currentMonth}`;
|
||||
|
||||
const [total, daily, monthly] = await Promise.all([
|
||||
@@ -534,7 +556,7 @@ class RedisClient {
|
||||
// 📊 获取今日系统统计
|
||||
async getTodayStats() {
|
||||
try {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const today = getDateStringInTimezone();
|
||||
const dailyKeys = await this.client.keys(`usage:daily:*:${today}`);
|
||||
|
||||
let totalRequestsToday = 0;
|
||||
|
||||
@@ -32,7 +32,9 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
tokenLimit,
|
||||
expiresAt,
|
||||
claudeAccountId,
|
||||
concurrencyLimit
|
||||
concurrencyLimit,
|
||||
enableModelRestriction,
|
||||
restrictedModels
|
||||
} = req.body;
|
||||
|
||||
// 输入验证
|
||||
@@ -57,13 +59,24 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
return res.status(400).json({ error: 'Concurrency limit must be a non-negative integer' });
|
||||
}
|
||||
|
||||
// 验证模型限制字段
|
||||
if (enableModelRestriction !== undefined && typeof enableModelRestriction !== 'boolean') {
|
||||
return res.status(400).json({ error: 'Enable model restriction must be a boolean' });
|
||||
}
|
||||
|
||||
if (restrictedModels !== undefined && !Array.isArray(restrictedModels)) {
|
||||
return res.status(400).json({ error: 'Restricted models must be an array' });
|
||||
}
|
||||
|
||||
const newKey = await apiKeyService.generateApiKey({
|
||||
name,
|
||||
description,
|
||||
tokenLimit,
|
||||
expiresAt,
|
||||
claudeAccountId,
|
||||
concurrencyLimit
|
||||
concurrencyLimit,
|
||||
enableModelRestriction,
|
||||
restrictedModels
|
||||
});
|
||||
|
||||
logger.success(`🔑 Admin created new API key: ${name}`);
|
||||
@@ -78,9 +91,9 @@ 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, claudeAccountId } = req.body;
|
||||
const { tokenLimit, concurrencyLimit, claudeAccountId, enableModelRestriction, restrictedModels } = req.body;
|
||||
|
||||
// 只允许更新tokenLimit、concurrencyLimit和claudeAccountId
|
||||
// 只允许更新指定字段
|
||||
const updates = {};
|
||||
|
||||
if (tokenLimit !== undefined && tokenLimit !== null && tokenLimit !== '') {
|
||||
@@ -102,6 +115,21 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
updates.claudeAccountId = claudeAccountId || '';
|
||||
}
|
||||
|
||||
// 处理模型限制字段
|
||||
if (enableModelRestriction !== undefined) {
|
||||
if (typeof enableModelRestriction !== 'boolean') {
|
||||
return res.status(400).json({ error: 'Enable model restriction must be a boolean' });
|
||||
}
|
||||
updates.enableModelRestriction = enableModelRestriction;
|
||||
}
|
||||
|
||||
if (restrictedModels !== undefined) {
|
||||
if (!Array.isArray(restrictedModels)) {
|
||||
return res.status(400).json({ error: 'Restricted models must be an array' });
|
||||
}
|
||||
updates.restrictedModels = restrictedModels;
|
||||
}
|
||||
|
||||
await apiKeyService.updateApiKey(keyId, updates);
|
||||
|
||||
logger.success(`📝 Admin updated API key: ${keyId}`);
|
||||
|
||||
@@ -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
|
||||
@@ -102,6 +108,14 @@ class ApiKeyService {
|
||||
|
||||
logger.api(`🔓 API key validated successfully: ${keyData.id}`);
|
||||
|
||||
// 解析限制模型数据
|
||||
let restrictedModels = [];
|
||||
try {
|
||||
restrictedModels = keyData.restrictedModels ? JSON.parse(keyData.restrictedModels) : [];
|
||||
} catch (e) {
|
||||
restrictedModels = [];
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
keyData: {
|
||||
@@ -109,7 +123,9 @@ class ApiKeyService {
|
||||
name: keyData.name,
|
||||
claudeAccountId: keyData.claudeAccountId,
|
||||
tokenLimit: parseInt(keyData.tokenLimit),
|
||||
concurrencyLimit: parseInt(keyData.concurrencyLimit || 0),
|
||||
concurrencyLimit: parseInt(keyData.concurrencyLimit || 0),
|
||||
enableModelRestriction: keyData.enableModelRestriction === 'true',
|
||||
restrictedModels: restrictedModels,
|
||||
usage
|
||||
}
|
||||
};
|
||||
@@ -131,6 +147,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 +172,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,34 @@ class ClaudeRelayService {
|
||||
let upstreamRequest = null;
|
||||
|
||||
try {
|
||||
// 调试日志:查看API Key数据
|
||||
logger.info(`🔍 API Key data received:`, {
|
||||
apiKeyName: apiKeyData.name,
|
||||
enableModelRestriction: apiKeyData.enableModelRestriction,
|
||||
restrictedModels: apiKeyData.restrictedModels,
|
||||
requestedModel: requestBody.model
|
||||
});
|
||||
|
||||
// 检查模型限制
|
||||
if (apiKeyData.enableModelRestriction && apiKeyData.restrictedModels && apiKeyData.restrictedModels.length > 0) {
|
||||
const requestedModel = requestBody.model;
|
||||
logger.info(`🔒 Model restriction check - Requested model: ${requestedModel}, Restricted models: ${JSON.stringify(apiKeyData.restrictedModels)}`);
|
||||
|
||||
if (requestedModel && apiKeyData.restrictedModels.includes(requestedModel)) {
|
||||
logger.warn(`🚫 Model restriction violation for key ${apiKeyData.name}: Attempted to use restricted model ${requestedModel}`);
|
||||
return {
|
||||
statusCode: 403,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
error: {
|
||||
type: 'forbidden',
|
||||
message: '暂无该模型访问权限'
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 生成会话哈希用于sticky会话
|
||||
const sessionHash = sessionHelper.generateSessionHash(requestBody);
|
||||
|
||||
@@ -419,6 +447,36 @@ class ClaudeRelayService {
|
||||
// 🌊 处理流式响应(带usage数据捕获)
|
||||
async relayStreamRequestWithUsageCapture(requestBody, apiKeyData, responseStream, clientHeaders, usageCallback) {
|
||||
try {
|
||||
// 调试日志:查看API Key数据(流式请求)
|
||||
logger.info(`🔍 [Stream] API Key data received:`, {
|
||||
apiKeyName: apiKeyData.name,
|
||||
enableModelRestriction: apiKeyData.enableModelRestriction,
|
||||
restrictedModels: apiKeyData.restrictedModels,
|
||||
requestedModel: requestBody.model
|
||||
});
|
||||
|
||||
// 检查模型限制
|
||||
if (apiKeyData.enableModelRestriction && apiKeyData.restrictedModels && apiKeyData.restrictedModels.length > 0) {
|
||||
const requestedModel = requestBody.model;
|
||||
logger.info(`🔒 [Stream] Model restriction check - Requested model: ${requestedModel}, Restricted models: ${JSON.stringify(apiKeyData.restrictedModels)}`);
|
||||
|
||||
if (requestedModel && apiKeyData.restrictedModels.includes(requestedModel)) {
|
||||
logger.warn(`🚫 Model restriction violation for key ${apiKeyData.name}: Attempted to use restricted model ${requestedModel}`);
|
||||
|
||||
// 对于流式响应,需要写入错误并结束流
|
||||
const errorResponse = JSON.stringify({
|
||||
error: {
|
||||
type: 'forbidden',
|
||||
message: '暂无该模型访问权限'
|
||||
}
|
||||
});
|
||||
|
||||
responseStream.writeHead(403, { 'Content-Type': 'application/json' });
|
||||
responseStream.end(errorResponse);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 生成会话哈希用于sticky会话
|
||||
const sessionHash = sessionHelper.generateSessionHash(requestBody);
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user