diff --git a/src/routes/admin.js b/src/routes/admin.js index 8c55fa87..1273370d 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -320,7 +320,8 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => { restrictedModels, enableClientRestriction, allowedClients, - dailyCostLimit + dailyCostLimit, + tags } = req.body; // 输入验证 @@ -371,6 +372,15 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => { return res.status(400).json({ error: 'Allowed clients must be an array' }); } + // 验证标签字段 + if (tags !== undefined && !Array.isArray(tags)) { + return res.status(400).json({ error: 'Tags must be an array' }); + } + + if (tags && tags.some(tag => typeof tag !== 'string' || tag.trim().length === 0)) { + return res.status(400).json({ error: 'All tags must be non-empty strings' }); + } + const newKey = await apiKeyService.generateApiKey({ name, description, @@ -386,7 +396,8 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => { restrictedModels, enableClientRestriction, allowedClients, - dailyCostLimit + dailyCostLimit, + tags }); logger.success(`🔑 Admin created new API key: ${name}`); @@ -401,7 +412,7 @@ 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, rateLimitWindow, rateLimitRequests, claudeAccountId, geminiAccountId, permissions, enableModelRestriction, restrictedModels, enableClientRestriction, allowedClients, expiresAt, dailyCostLimit } = req.body; + const { tokenLimit, concurrencyLimit, rateLimitWindow, rateLimitRequests, claudeAccountId, geminiAccountId, permissions, enableModelRestriction, restrictedModels, enableClientRestriction, allowedClients, expiresAt, dailyCostLimit, tags } = req.body; // 只允许更新指定字段 const updates = {}; @@ -506,6 +517,17 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => { updates.dailyCostLimit = costLimit; } + // 处理标签 + if (tags !== undefined) { + if (!Array.isArray(tags)) { + return res.status(400).json({ error: 'Tags must be an array' }); + } + if (tags.some(tag => typeof tag !== 'string' || tag.trim().length === 0)) { + return res.status(400).json({ error: 'All tags must be non-empty strings' }); + } + updates.tags = tags; + } + await apiKeyService.updateApiKey(keyId, updates); logger.success(`📝 Admin updated API key: ${keyId}`); diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index f7a1797b..79dbf930 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -27,7 +27,8 @@ class ApiKeyService { restrictedModels = [], enableClientRestriction = false, allowedClients = [], - dailyCostLimit = 0 + dailyCostLimit = 0, + tags = [] } = options; // 生成简单的API Key (64字符十六进制) @@ -53,6 +54,7 @@ class ApiKeyService { enableClientRestriction: String(enableClientRestriction || false), allowedClients: JSON.stringify(allowedClients || []), dailyCostLimit: String(dailyCostLimit || 0), + tags: JSON.stringify(tags || []), createdAt: new Date().toISOString(), lastUsedAt: '', expiresAt: expiresAt || '', @@ -82,6 +84,7 @@ class ApiKeyService { enableClientRestriction: keyData.enableClientRestriction === 'true', allowedClients: JSON.parse(keyData.allowedClients || '[]'), dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0), + tags: JSON.parse(keyData.tags || '[]'), createdAt: keyData.createdAt, expiresAt: keyData.expiresAt, createdBy: keyData.createdBy @@ -142,6 +145,14 @@ class ApiKeyService { allowedClients = []; } + // 解析标签 + let tags = []; + try { + tags = keyData.tags ? JSON.parse(keyData.tags) : []; + } catch (e) { + tags = []; + } + return { valid: true, keyData: { @@ -163,6 +174,7 @@ class ApiKeyService { allowedClients: allowedClients, dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0), dailyCost: dailyCost || 0, + tags: tags, usage } }; @@ -201,6 +213,11 @@ class ApiKeyService { } catch (e) { key.allowedClients = []; } + try { + key.tags = key.tags ? JSON.parse(key.tags) : []; + } catch (e) { + key.tags = []; + } delete key.apiKey; // 不返回哈希后的key } @@ -220,12 +237,12 @@ class ApiKeyService { } // 允许更新的字段 - const allowedUpdates = ['name', 'description', 'tokenLimit', 'concurrencyLimit', 'rateLimitWindow', 'rateLimitRequests', 'isActive', 'claudeAccountId', 'geminiAccountId', 'permissions', 'expiresAt', 'enableModelRestriction', 'restrictedModels', 'enableClientRestriction', 'allowedClients', 'dailyCostLimit']; + const allowedUpdates = ['name', 'description', 'tokenLimit', 'concurrencyLimit', 'rateLimitWindow', 'rateLimitRequests', 'isActive', 'claudeAccountId', 'geminiAccountId', 'permissions', 'expiresAt', 'enableModelRestriction', 'restrictedModels', 'enableClientRestriction', 'allowedClients', 'dailyCostLimit', 'tags']; const updatedData = { ...keyData }; for (const [field, value] of Object.entries(updates)) { if (allowedUpdates.includes(field)) { - if (field === 'restrictedModels' || field === 'allowedClients') { + if (field === 'restrictedModels' || field === 'allowedClients' || field === 'tags') { // 特殊处理数组字段 updatedData[field] = JSON.stringify(value || []); } else if (field === 'enableModelRestriction' || field === 'enableClientRestriction') { diff --git a/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue b/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue index 502bfd34..c07e3a17 100644 --- a/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue +++ b/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue @@ -29,6 +29,40 @@ > + +
用于标记不同团队或用途,方便筛选管理
+名称不可修改
用于标记不同团队或用途,方便筛选管理
+