diff --git a/README.md b/README.md index 30dcfb50..cbeeb9d2 100644 --- a/README.md +++ b/README.md @@ -483,6 +483,7 @@ model_provider = "crs" model = "gpt-5" model_reasoning_effort = "high" disable_response_storage = true +preferred_auth_method = "apikey" [model_providers.crs] name = "crs" @@ -494,7 +495,7 @@ wire_api = "responses" ```json { - "OPENAI_API_KEY": "你的后台创建的API密钥" + "OPENAI_API_KEY": "你的后台创建的API密钥" } ``` diff --git a/src/routes/apiStats.js b/src/routes/apiStats.js index cac72503..4dca7386 100644 --- a/src/routes/apiStats.js +++ b/src/routes/apiStats.js @@ -31,8 +31,8 @@ router.post('/api/get-key-id', async (req, res) => { }) } - // 验证API Key - const validation = await apiKeyService.validateApiKey(apiKey) + // 验证API Key(使用不触发激活的验证方法) + const validation = await apiKeyService.validateApiKeyForStats(apiKey) if (!validation.valid) { const clientIP = req.ip || req.connection?.remoteAddress || 'unknown' @@ -146,6 +146,11 @@ router.post('/api/user-stats', async (req, res) => { enableClientRestriction: keyData.enableClientRestriction === 'true', allowedClients, permissions: keyData.permissions || 'all', + // 添加激活相关字段 + expirationMode: keyData.expirationMode || 'fixed', + isActivated: keyData.isActivated === 'true', + activationDays: parseInt(keyData.activationDays || 0), + activatedAt: keyData.activatedAt || null, usage // 使用完整的 usage 数据,而不是只有 total } } else if (apiKey) { @@ -158,8 +163,8 @@ router.post('/api/user-stats', async (req, res) => { }) } - // 验证API Key(重用现有的验证逻辑) - const validation = await apiKeyService.validateApiKey(apiKey) + // 验证API Key(使用不触发激活的验证方法) + const validation = await apiKeyService.validateApiKeyForStats(apiKey) if (!validation.valid) { const clientIP = req.ip || req.connection?.remoteAddress || 'unknown' @@ -335,6 +340,11 @@ router.post('/api/user-stats', async (req, res) => { isActive: true, // 如果能通过validateApiKey验证,说明一定是激活的 createdAt: keyData.createdAt, expiresAt: keyData.expiresAt, + // 添加激活相关字段 + expirationMode: keyData.expirationMode || 'fixed', + isActivated: keyData.isActivated === 'true', + activationDays: parseInt(keyData.activationDays || 0), + activatedAt: keyData.activatedAt || null, permissions: fullKeyData.permissions, // 使用统计(使用验证结果中的完整数据) diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index 8ee94337..7e7f5fde 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -256,6 +256,126 @@ class ApiKeyService { } } + // 🔍 验证API Key(仅用于统计查询,不触发激活) + async validateApiKeyForStats(apiKey) { + try { + if (!apiKey || !apiKey.startsWith(this.prefix)) { + return { valid: false, error: 'Invalid API key format' } + } + + // 计算API Key的哈希值 + const hashedKey = this._hashApiKey(apiKey) + + // 通过哈希值直接查找API Key(性能优化) + const keyData = await redis.findApiKeyByHash(hashedKey) + + if (!keyData) { + return { valid: false, error: 'API key not found' } + } + + // 检查是否激活 + if (keyData.isActive !== 'true') { + return { valid: false, error: 'API key is disabled' } + } + + // 注意:这里不处理激活逻辑,保持 API Key 的未激活状态 + + // 检查是否过期(仅对已激活的 Key 检查) + if ( + keyData.isActivated === 'true' && + keyData.expiresAt && + new Date() > new Date(keyData.expiresAt) + ) { + return { valid: false, error: 'API key has expired' } + } + + // 如果API Key属于某个用户,检查用户是否被禁用 + if (keyData.userId) { + try { + const userService = require('./userService') + const user = await userService.getUserById(keyData.userId, false) + if (!user || !user.isActive) { + return { valid: false, error: 'User account is disabled' } + } + } catch (userError) { + // 如果用户服务出错,记录但不影响API Key验证 + logger.warn(`Failed to check user status for API key ${keyData.id}:`, userError) + } + } + + // 获取当日费用 + const dailyCost = (await redis.getDailyCost(keyData.id)) || 0 + + // 获取使用统计 + const usage = await redis.getUsageStats(keyData.id) + + // 解析限制模型数据 + let restrictedModels = [] + try { + restrictedModels = keyData.restrictedModels ? JSON.parse(keyData.restrictedModels) : [] + } catch (e) { + restrictedModels = [] + } + + // 解析允许的客户端 + let allowedClients = [] + try { + allowedClients = keyData.allowedClients ? JSON.parse(keyData.allowedClients) : [] + } catch (e) { + allowedClients = [] + } + + // 解析标签 + let tags = [] + try { + tags = keyData.tags ? JSON.parse(keyData.tags) : [] + } catch (e) { + tags = [] + } + + return { + valid: true, + keyData: { + id: keyData.id, + name: keyData.name, + description: keyData.description, + createdAt: keyData.createdAt, + expiresAt: keyData.expiresAt, + // 添加激活相关字段 + expirationMode: keyData.expirationMode || 'fixed', + isActivated: keyData.isActivated === 'true', + activationDays: parseInt(keyData.activationDays || 0), + activatedAt: keyData.activatedAt || null, + claudeAccountId: keyData.claudeAccountId, + claudeConsoleAccountId: keyData.claudeConsoleAccountId, + geminiAccountId: keyData.geminiAccountId, + openaiAccountId: keyData.openaiAccountId, + azureOpenaiAccountId: keyData.azureOpenaiAccountId, + bedrockAccountId: keyData.bedrockAccountId, + permissions: keyData.permissions || 'all', + tokenLimit: parseInt(keyData.tokenLimit), + concurrencyLimit: parseInt(keyData.concurrencyLimit || 0), + rateLimitWindow: parseInt(keyData.rateLimitWindow || 0), + rateLimitRequests: parseInt(keyData.rateLimitRequests || 0), + rateLimitCost: parseFloat(keyData.rateLimitCost || 0), + enableModelRestriction: keyData.enableModelRestriction === 'true', + restrictedModels, + enableClientRestriction: keyData.enableClientRestriction === 'true', + allowedClients, + dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0), + weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0), + dailyCost: dailyCost || 0, + weeklyOpusCost: (await redis.getWeeklyOpusCost(keyData.id)) || 0, + tags, + usage + } + } + } catch (error) { + logger.error('❌ API key validation error (stats):', error) + return { valid: false, error: 'Internal validation error' } + } + } + // 📋 获取所有API Keys async getAllApiKeys(includeDeleted = false) { try { diff --git a/web/admin-spa/src/components/apistats/StatsOverview.vue b/web/admin-spa/src/components/apistats/StatsOverview.vue index 557166b1..5d4a9a6d 100644 --- a/web/admin-spa/src/components/apistats/StatsOverview.vue +++ b/web/admin-spa/src/components/apistats/StatsOverview.vue @@ -115,7 +115,19 @@ 过期时间 -
+ +
+ + 未激活 + (首次使用后{{ statsData.activationDays || 30 }}天过期) +
+ +
+
永不过期 diff --git a/web/admin-spa/src/views/TutorialView.vue b/web/admin-spa/src/views/TutorialView.vue index eb2e9cc3..ac938825 100644 --- a/web/admin-spa/src/views/TutorialView.vue +++ b/web/admin-spa/src/views/TutorialView.vue @@ -434,6 +434,7 @@
model = "gpt-5"
model_reasoning_effort = "high"
disable_response_storage = true
+
preferred_auth_method = "apikey"
[model_providers.crs]
name = "crs"
@@ -920,6 +921,7 @@
model = "gpt-5"
model_reasoning_effort = "high"
disable_response_storage = true
+
preferred_auth_method = "apikey"
[model_providers.crs]
name = "crs"
@@ -1397,6 +1399,7 @@
model = "gpt-5"
model_reasoning_effort = "high"
disable_response_storage = true
+
preferred_auth_method = "apikey"
[model_providers.crs]
name = "crs"