mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
feat(admin-spa): 添加 API Key 标签管理功能
基于 PR #114 的功能需求,为新版 admin-spa 实现完整的标签系统: 后端改进: - apiKeyService 支持标签的创建、查询和更新 - admin 路由添加标签验证和处理逻辑 - 标签以 JSON 数组形式存储在 Redis 中 前端功能: - API Key 列表增加标签列,显示彩色标签徽章 - 添加标签筛选器,支持按标签过滤 API Keys - 创建和编辑 API Key 时可添加/删除标签 - 标签输入支持 Enter 键快速添加 - 自动收集并排序所有可用标签 界面优化: - 使用蓝色圆角标签样式,视觉清晰 - 无标签时显示"无标签"提示 - 标签管理操作流畅,支持即时添加删除 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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}`);
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user