From 3c5068866ce637f7b49103773483d6ed7d690b8a Mon Sep 17 00:00:00 2001 From: Wesley Liddick Date: Wed, 10 Sep 2025 14:37:52 +0800 Subject: [PATCH] =?UTF-8?q?Revert=20"=E5=90=88=E5=B9=B6=E6=89=80=E6=9C=89?= =?UTF-8?q?=E6=96=B0=E5=8A=9F=E8=83=BD=E5=88=B0Wei-Shaw=E4=BB=93=E5=BA=93?= =?UTF-8?q?=EF=BC=88=E6=8E=92=E9=99=A4ApiStatsView.vue=EF=BC=89"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION | 2 +- src/middleware/auth.js | 88 +-- src/models/redis.js | 116 ++-- src/routes/admin.js | 108 ++-- src/routes/openaiRoutes.js | 227 +------ src/services/apiKeyService.js | 8 +- src/services/geminiAccountService.js | 6 +- .../components/apikeys/CreateApiKeyModal.vue | 183 +----- .../components/apikeys/EditApiKeyModal.vue | 92 +-- .../components/apikeys/LimitProgressBar.vue | 40 +- web/admin-spa/src/views/ApiKeysView.vue | 106 ++- web/admin-spa/src/views/ApiStatsView.vue | 604 ++++++++++++++++++ 12 files changed, 805 insertions(+), 775 deletions(-) create mode 100644 web/admin-spa/src/views/ApiStatsView.vue diff --git a/VERSION b/VERSION index 8b8c5239..eef0324e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.135 +1.1.134 diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 95408797..4d4364ac 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -373,92 +373,6 @@ const authenticateApiKey = async (req, res, next) => { } } - // 🔒 安全检查 GPT-5 High 推理级别周费用限制 - const weeklyGPT5HighCostLimit = parseFloat(validation.keyData.weeklyGPT5HighCostLimit) || 0 - - if (weeklyGPT5HighCostLimit > 0) { - try { - // 从请求中获取模型和推理级别信息 - const requestBody = req.body || {} - const model = String(requestBody.model || '').toLowerCase() - - // 只对 GPT-5 模型进行检查 - if (model.includes('gpt-5')) { - // 安全提取推理级别 - let detectedLevel = 'medium' // 默认值 - - try { - // 多种方式提取推理级别 - detectedLevel = requestBody.reasoning_effort || - requestBody.model_reasoning_effort || - req.headers['reasoning-effort'] || - 'medium' - - // 检查 reasoning 字段 - const reasoningField = requestBody.reasoning - if (reasoningField) { - if (typeof reasoningField === 'string') { - if (reasoningField.includes('high') || reasoningField.includes('maximum')) { - detectedLevel = 'high' - } - } else if (typeof reasoningField === 'object' && reasoningField.effort) { - detectedLevel = reasoningField.effort - } - } - - // 确保级别值是字符串 - detectedLevel = String(detectedLevel).toLowerCase() - } catch (levelError) { - logger.warn('Error extracting reasoning level, using default:', levelError) - detectedLevel = 'medium' - } - - // 只对 High 级别进行限制检查 - if (detectedLevel === 'high') { - const weeklyGPT5HighCost = parseFloat(validation.keyData.weeklyGPT5HighCost) || 0 - - if (weeklyGPT5HighCost >= weeklyGPT5HighCostLimit) { - logger.security( - `💰 Weekly GPT-5 High cost limit exceeded for key: ${validation.keyData.id} (${validation.keyData.name}), cost: $${weeklyGPT5HighCost.toFixed(2)}/$${weeklyGPT5HighCostLimit}` - ) - - // 安全计算下周一的重置时间 - let resetDate - try { - const now = new Date() - const nextMonday = new Date(now) - nextMonday.setDate(now.getDate() + (7 - now.getDay() + 1) % 7 || 7) - nextMonday.setHours(0, 0, 0, 0) - resetDate = nextMonday - } catch (dateError) { - logger.warn('Error calculating reset date:', dateError) - resetDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7天后 - } - - return res.status(429).json({ - error: 'Weekly GPT-5 High cost limit exceeded', - message: `已达到 GPT-5 High推理级别周费用限制 ($${weeklyGPT5HighCostLimit})`, - currentCost: weeklyGPT5HighCost, - costLimit: weeklyGPT5HighCostLimit, - reasoningLevel: 'high', - resetAt: resetDate.toISOString() - }) - } - - // 记录使用情况(不影响性能) - if (weeklyGPT5HighCostLimit > 0) { - logger.api( - `💰 GPT-5 High weekly cost usage for key: ${validation.keyData.id} (${validation.keyData.name}), current: $${weeklyGPT5HighCost.toFixed(2)}/$${weeklyGPT5HighCostLimit} (${((weeklyGPT5HighCost / weeklyGPT5HighCostLimit) * 100).toFixed(1)}%)` - ) - } - } - } - } catch (gpt5Error) { - logger.warn('Error in GPT-5 High cost check, continuing with request:', gpt5Error) - // 发生错误时不阻止请求,确保服务可用性 - } - } - // 将验证信息添加到请求对象(只包含必要信息) req.apiKey = { id: validation.keyData.id, @@ -1206,4 +1120,4 @@ module.exports = { errorHandler, globalRateLimit, requestSizeLimit -} \ No newline at end of file +} diff --git a/src/models/redis.js b/src/models/redis.js index cd5046f4..7ef64ebf 100644 --- a/src/models/redis.js +++ b/src/models/redis.js @@ -733,62 +733,6 @@ class RedisClient { logger.debug(`💰 Opus cost incremented successfully, new weekly total: $${results[0][1]}`) } - // 💰 获取本周 GPT-5 High 费用 - async getWeeklyGPT5HighCost(keyId) { - try { - if (!keyId) { - logger.warn('getWeeklyGPT5HighCost: keyId is required') - return 0 - } - - const currentWeek = getWeekStringInTimezone() - const costKey = `usage:gpt5-high:weekly:${keyId}:${currentWeek}` - const cost = await this.client.get(costKey) - const result = parseFloat(cost || 0) - - logger.debug( - `💰 Getting weekly GPT-5 High cost for ${keyId}, week: ${currentWeek}, result: $${result.toFixed(4)}` - ) - return result - } catch (error) { - logger.error('Error getting weekly GPT-5 High cost:', error) - return 0 // 发生错误时返回0,不影响正常流程 - } - } - - // 💰 增加本周 GPT-5 High 费用 - async incrementWeeklyGPT5HighCost(keyId, amount) { - try { - if (!keyId || !amount || amount <= 0) { - logger.warn('incrementWeeklyGPT5HighCost: invalid parameters', { keyId, amount }) - return - } - - const currentWeek = getWeekStringInTimezone() - const weeklyKey = `usage:gpt5-high:weekly:${keyId}:${currentWeek}` - const totalKey = `usage:gpt5-high:total:${keyId}` - - logger.debug( - `💰 Incrementing weekly GPT-5 High cost for ${keyId}, week: ${currentWeek}, amount: $${amount.toFixed(4)}` - ) - - // 使用 pipeline 批量执行,提高性能和一致性 - const pipeline = this.client.pipeline() - pipeline.incrbyfloat(weeklyKey, amount) - pipeline.incrbyfloat(totalKey, amount) - pipeline.expire(weeklyKey, 60 * 60 * 24 * 14) // 2周后过期 - - await pipeline.exec() - - logger.debug( - `💰 Weekly GPT-5 High cost updated successfully for ${keyId}` - ) - } catch (error) { - logger.error('Error incrementing weekly GPT-5 High cost:', error) - // 不抛出错误,避免影响主流程 - } - } - // 💰 计算账户的每日费用(基于模型使用) async getAccountDailyCost(accountId) { const CostCalculator = require('../utils/costCalculator') @@ -1412,9 +1356,12 @@ class RedisClient { } // 🔗 会话sticky映射管理 - async setSessionAccountMapping(sessionHash, accountId, ttl = 3600) { + async setSessionAccountMapping(sessionHash, accountId, ttl = null) { + const appConfig = require('../../config/config') + // 从配置读取TTL(小时),转换为秒,默认1小时 + const defaultTTL = ttl !== null ? ttl : (appConfig.session?.stickyTtlHours || 1) * 60 * 60 const key = `sticky_session:${sessionHash}` - await this.client.set(key, accountId, 'EX', ttl) + await this.client.set(key, accountId, 'EX', defaultTTL) } async getSessionAccountMapping(sessionHash) { @@ -1422,6 +1369,57 @@ class RedisClient { return await this.client.get(key) } + // 🚀 智能会话TTL续期:剩余时间少于阈值时自动续期 + async extendSessionAccountMappingTTL(sessionHash) { + const appConfig = require('../../config/config') + const key = `sticky_session:${sessionHash}` + + // 📊 从配置获取参数 + const ttlHours = appConfig.session?.stickyTtlHours || 1 // 小时,默认1小时 + const thresholdMinutes = appConfig.session?.renewalThresholdMinutes || 0 // 分钟,默认0(不续期) + + // 如果阈值为0,不执行续期 + if (thresholdMinutes === 0) { + return true + } + + const fullTTL = ttlHours * 60 * 60 // 转换为秒 + const renewalThreshold = thresholdMinutes * 60 // 转换为秒 + + try { + // 获取当前剩余TTL(秒) + const remainingTTL = await this.client.ttl(key) + + // 键不存在或已过期 + if (remainingTTL === -2) { + return false + } + + // 键存在但没有TTL(永不过期,不需要处理) + if (remainingTTL === -1) { + return true + } + + // 🎯 智能续期策略:仅在剩余时间少于阈值时才续期 + if (remainingTTL < renewalThreshold) { + await this.client.expire(key, fullTTL) + logger.debug( + `🔄 Renewed sticky session TTL: ${sessionHash} (was ${Math.round(remainingTTL / 60)}min, renewed to ${ttlHours}h)` + ) + return true + } + + // 剩余时间充足,无需续期 + logger.debug( + `✅ Sticky session TTL sufficient: ${sessionHash} (remaining ${Math.round(remainingTTL / 60)}min)` + ) + return true + } catch (error) { + logger.error('❌ Failed to extend session TTL:', error) + return false + } + } + async deleteSessionAccountMapping(sessionHash) { const key = `sticky_session:${sessionHash}` return await this.client.del(key) @@ -1709,4 +1707,4 @@ redisClient.getDateStringInTimezone = getDateStringInTimezone redisClient.getHourInTimezone = getHourInTimezone redisClient.getWeekStringInTimezone = getWeekStringInTimezone -module.exports = redisClient \ No newline at end of file +module.exports = redisClient diff --git a/src/routes/admin.js b/src/routes/admin.js index 8732a52f..699be503 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -122,7 +122,7 @@ router.get('/api-keys/:keyId/cost-debug', authenticateAdmin, async (req, res) => // 获取所有API Keys router.get('/api-keys', authenticateAdmin, async (req, res) => { try { - const { timeRange = 'all' } = req.query // all, 7days, monthly + const { timeRange = 'all', startDate, endDate } = req.query // all, 7days, monthly, custom const apiKeys = await apiKeyService.getAllApiKeys() // 获取用户服务来补充owner信息 @@ -132,7 +132,32 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => { const now = new Date() const searchPatterns = [] - if (timeRange === 'today') { + if (timeRange === 'custom' && startDate && endDate) { + // 自定义日期范围 + const redisClient = require('../models/redis') + const start = new Date(startDate) + const end = new Date(endDate) + + // 确保日期范围有效 + if (start > end) { + return res.status(400).json({ error: 'Start date must be before or equal to end date' }) + } + + // 限制最大范围为365天 + const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1 + if (daysDiff > 365) { + return res.status(400).json({ error: 'Date range cannot exceed 365 days' }) + } + + // 生成日期范围内每天的搜索模式 + const currentDate = new Date(start) + while (currentDate <= end) { + const tzDate = redisClient.getDateInTimezone(currentDate) + const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}` + searchPatterns.push(`usage:daily:*:${dateStr}`) + currentDate.setDate(currentDate.getDate() + 1) + } + } else if (timeRange === 'today') { // 今日 - 使用时区日期 const redisClient = require('../models/redis') const tzDate = redisClient.getDateInTimezone(now) @@ -233,7 +258,7 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => { apiKey.usage.total.formattedCost = CostCalculator.formatCost(totalCost) } } else { - // 7天或本月:重新计算统计数据 + // 7天、本月或自定义日期范围:重新计算统计数据 const tempUsage = { requests: 0, tokens: 0, @@ -274,12 +299,28 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => { const tzDate = redisClient.getDateInTimezone(now) const tzMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}` - const modelKeys = - timeRange === 'today' - ? await client.keys(`usage:${apiKey.id}:model:daily:*:${tzToday}`) - : timeRange === '7days' - ? await client.keys(`usage:${apiKey.id}:model:daily:*:*`) - : await client.keys(`usage:${apiKey.id}:model:monthly:*:${tzMonth}`) + let modelKeys = [] + if (timeRange === 'custom' && startDate && endDate) { + // 自定义日期范围:获取范围内所有日期的模型统计 + const start = new Date(startDate) + const end = new Date(endDate) + const currentDate = new Date(start) + + while (currentDate <= end) { + const tzDateForKey = redisClient.getDateInTimezone(currentDate) + const dateStr = `${tzDateForKey.getUTCFullYear()}-${String(tzDateForKey.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDateForKey.getUTCDate()).padStart(2, '0')}` + const dayKeys = await client.keys(`usage:${apiKey.id}:model:daily:*:${dateStr}`) + modelKeys = modelKeys.concat(dayKeys) + currentDate.setDate(currentDate.getDate() + 1) + } + } else { + modelKeys = + timeRange === 'today' + ? await client.keys(`usage:${apiKey.id}:model:daily:*:${tzToday}`) + : timeRange === '7days' + ? await client.keys(`usage:${apiKey.id}:model:daily:*:*`) + : await client.keys(`usage:${apiKey.id}:model:monthly:*:${tzMonth}`) + } const modelStatsMap = new Map() @@ -295,8 +336,8 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => { continue } } - } else if (timeRange === 'today') { - // today选项已经在查询时过滤了,不需要额外处理 + } else if (timeRange === 'today' || timeRange === 'custom') { + // today和custom选项已经在查询时过滤了,不需要额外处理 } const modelMatch = key.match( @@ -947,10 +988,8 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => { expiresAt, dailyCostLimit, weeklyOpusCostLimit, - weeklyGPT5HighCostLimit, tags, - ownerId, // 新增:所有者ID字段 - icon // 新增:图标(base64编码) + ownerId // 新增:所有者ID字段 } = req.body // 只允许更新指定字段 @@ -1113,22 +1152,6 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => { updates.weeklyOpusCostLimit = costLimit } - // 处理 GPT-5 High 周费用限制 - if ( - weeklyGPT5HighCostLimit !== undefined && - weeklyGPT5HighCostLimit !== null && - weeklyGPT5HighCostLimit !== '' - ) { - const costLimit = Number(weeklyGPT5HighCostLimit) - // 明确验证非负数(0 表示禁用,负数无意义) - if (isNaN(costLimit) || costLimit < 0) { - return res - .status(400) - .json({ error: 'Weekly GPT-5 High cost limit must be a non-negative number' }) - } - updates.weeklyGPT5HighCostLimit = costLimit - } - // 处理标签 if (tags !== undefined) { if (!Array.isArray(tags)) { @@ -1140,19 +1163,6 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => { updates.tags = tags } - // 处理图标 - if (icon !== undefined) { - // icon 可以是空字符串(清除图标)或 base64 编码的字符串 - if (icon !== '' && typeof icon !== 'string') { - return res.status(400).json({ error: 'Icon must be a string' }) - } - // 简单验证 base64 格式(如果不为空) - if (icon && !icon.startsWith('data:image/')) { - return res.status(400).json({ error: 'Icon must be a valid base64 image' }) - } - updates.icon = icon - } - // 处理活跃/禁用状态状态, 放在过期处理后,以确保后续增加禁用key功能 if (isActive !== undefined) { if (typeof isActive !== 'boolean') { @@ -3913,10 +3923,10 @@ router.get('/model-stats', authenticateAdmin, async (req, res) => { return res.status(400).json({ error: 'Start date must be before or equal to end date' }) } - // 限制最大范围为31天 + // 限制最大范围为365天 const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1 - if (daysDiff > 31) { - return res.status(400).json({ error: 'Date range cannot exceed 31 days' }) + if (daysDiff > 365) { + return res.status(400).json({ error: 'Date range cannot exceed 365 days' }) } // 生成日期范围内所有日期的搜索模式 @@ -4377,10 +4387,10 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) = return res.status(400).json({ error: 'Start date must be before or equal to end date' }) } - // 限制最大范围为31天 + // 限制最大范围为365天 const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1 - if (daysDiff > 31) { - return res.status(400).json({ error: 'Date range cannot exceed 31 days' }) + if (daysDiff > 365) { + return res.status(400).json({ error: 'Date range cannot exceed 365 days' }) } // 生成日期范围内所有日期的搜索模式 diff --git a/src/routes/openaiRoutes.js b/src/routes/openaiRoutes.js index cd22e535..dfe27719 100644 --- a/src/routes/openaiRoutes.js +++ b/src/routes/openaiRoutes.js @@ -2,31 +2,13 @@ const express = require('express') const axios = require('axios') const router = express.Router() const logger = require('../utils/logger') +const config = require('../../config/config') const { authenticateApiKey } = require('../middleware/auth') const unifiedOpenAIScheduler = require('../services/unifiedOpenAIScheduler') const openaiAccountService = require('../services/openaiAccountService') const apiKeyService = require('../services/apiKeyService') const crypto = require('crypto') const ProxyHelper = require('../utils/proxyHelper') -const redis = require('../models/redis') // 新增:用于GPT-5 High费用记录 - -// 🔥 计算GPT-5 High推理级别的额外费用 -function calculateGPT5HighCost(usageData) { - if (!usageData) return 0 - - // GPT-5 High推理级别费用(示例费率,实际需要根据OpenAI官方定价调整) - const inputTokens = usageData.prompt_tokens || 0 - const outputTokens = usageData.completion_tokens || 0 - - // High推理级别的额外费用(美元) - const inputCostPerToken = 0.00002 // $0.02 per 1K tokens for input - const outputCostPerToken = 0.0001 // $0.10 per 1K tokens for output - - const inputCost = (inputTokens / 1000) * inputCostPerToken - const outputCost = (outputTokens / 1000) * outputCostPerToken - - return inputCost + outputCost -} // 创建代理 Agent(使用统一的代理工具) function createProxyAgent(proxy) { @@ -122,103 +104,7 @@ const handleResponses = async (req, res) => { null // 从请求体中提取模型和流式标志 - const originalModel = req.body?.model || null // 保存原始模型名称用于限制检查 - let requestedModel = originalModel - - // 🔍 详细分析 Codex CLI 请求格式(用于推理级别识别) - logger.info(`🔍 Codex CLI request analysis:`, { - model: req.body?.model, - temperature: req.body?.temperature, - max_tokens: req.body?.max_tokens, - reasoning_effort: req.body?.reasoning_effort, - model_reasoning_effort: req.body?.model_reasoning_effort, - stream: req.body?.stream, - allHeaders: Object.keys(req.headers), - allBodyKeys: Object.keys(req.body || {}) - }) - - // 🎯 尝试从请求中识别推理级别 - let effectiveModel = originalModel - - // 检查所有可能的推理级别字段 - const reasoningEffort = - req.body?.reasoning_effort || - req.body?.model_reasoning_effort || - req.headers['reasoning-effort'] - - // 🔥 检查 reasoning 字段(可能包含推理级别信息) - const reasoningField = req.body?.reasoning - - logger.info(`🔥 Detailed reasoning analysis:`, { - reasoningField, - reasoningEffort, - reasoningType: typeof reasoningField - }) - - // 如果是 GPT-5,尝试从各种字段中提取推理级别 - if (originalModel === 'gpt-5') { - let detectedLevel = null - - // 方法1: 直接从 reasoning_effort 获取 - if (reasoningEffort) { - detectedLevel = reasoningEffort - } - // 方法2: 从 reasoning 字段分析(可能是对象或字符串) - else if (reasoningField) { - if (typeof reasoningField === 'string') { - // 如果是字符串,查找级别关键词 - if (reasoningField.includes('high') || reasoningField.includes('maximum')) { - detectedLevel = 'high' - } else if (reasoningField.includes('medium') || reasoningField.includes('balanced')) { - detectedLevel = 'medium' - } else if (reasoningField.includes('low') || reasoningField.includes('fast')) { - detectedLevel = 'low' - } else if (reasoningField.includes('minimal') || reasoningField.includes('quick')) { - detectedLevel = 'minimal' - } - } else if (typeof reasoningField === 'object') { - // 检查 effort 字段 (Codex CLI 使用这种格式) - if (reasoningField.effort) { - detectedLevel = reasoningField.effort - } else if (reasoningField.level) { - detectedLevel = reasoningField.level - } - } - } - - if (detectedLevel) { - effectiveModel = `gpt-5 ${detectedLevel}` - logger.info( - `🎯 Detected GPT-5 with reasoning level: ${detectedLevel} → Effective model for restriction: ${effectiveModel}` - ) - } - } - - // 🚀 检查模型限制(使用有效模型名称) - if ( - apiKeyData.enableModelRestriction && - apiKeyData.restrictedModels && - apiKeyData.restrictedModels.length > 0 - ) { - logger.info( - `🔒 OpenAI Model restriction check - Original: ${originalModel}, Effective: ${effectiveModel}, Restricted: ${JSON.stringify(apiKeyData.restrictedModels)}` - ) - - if (effectiveModel && apiKeyData.restrictedModels.includes(effectiveModel)) { - logger.warn( - `🚫 OpenAI Model restriction violation for key ${apiKeyData.name}: Attempted to use restricted model ${effectiveModel}` - ) - return res.status(403).json({ - error: { - type: 'forbidden', - message: '暂无该模型访问权限', - code: 'model_restricted' - } - }) - } - } - - // 如果通过限制检查,再进行模型规范化 + let requestedModel = req.body?.model || null // 如果模型是 gpt-5 开头且后面还有内容(如 gpt-5-2025-08-07),则覆盖为 gpt-5 if (requestedModel && requestedModel.startsWith('gpt-5-') && requestedModel !== 'gpt-5') { @@ -294,7 +180,7 @@ const handleResponses = async (req, res) => { // 配置请求选项 const axiosConfig = { headers, - timeout: 60 * 1000 * 10, + timeout: config.requestTimeout || 600000, validateStatus: () => true } @@ -482,61 +368,6 @@ const handleResponses = async (req, res) => { logger.info( `📊 Recorded OpenAI non-stream usage - Input: ${inputTokens}, Output: ${outputTokens}, Total: ${usageData.total_tokens || inputTokens + outputTokens}, Model: ${actualModel}` ) - - // 🔥 安全记录 GPT-5 High 推理级别费用(非流式响应) - try { - if (actualModel && String(actualModel).toLowerCase().includes('gpt-5')) { - // 安全提取推理级别 - const originalRequestBody = req.body || {} - let detectedLevel = 'medium' // 安全默认值 - - try { - detectedLevel = - originalRequestBody.reasoning_effort || - originalRequestBody.model_reasoning_effort || - 'medium' - - // 检查 reasoning 字段 - const reasoningField = originalRequestBody.reasoning - if (reasoningField) { - if (typeof reasoningField === 'string') { - if (reasoningField.includes('high') || reasoningField.includes('maximum')) { - detectedLevel = 'high' - } - } else if (typeof reasoningField === 'object' && reasoningField.effort) { - detectedLevel = reasoningField.effort - } - } - } catch (levelError) { - logger.debug( - 'Error extracting reasoning level for cost recording (non-stream):', - levelError - ) - } - - // 如果是 High 级别,记录额外的费用 - if (String(detectedLevel).toLowerCase() === 'high') { - const gpt5HighCost = calculateGPT5HighCost(usageData) - - if (gpt5HighCost > 0) { - // 记录GPT-5 High专门的费用统计(用于周限制) - await redis.incrementWeeklyGPT5HighCost(apiKeyData.id, gpt5HighCost) - logger.info( - `💰 Recorded GPT-5 High weekly cost (non-stream): $${gpt5HighCost.toFixed(4)} for key ${apiKeyData.id} (${apiKeyData.name})` - ) - - // 🔧 关键修复:同时记录到常规费用统计中 - await redis.incrementDailyCost(apiKeyData.id, gpt5HighCost) - logger.info( - `💰 Recorded GPT-5 High to daily cost (non-stream): $${gpt5HighCost.toFixed(4)} for key ${apiKeyData.id} (${apiKeyData.name})` - ) - } - } - } - } catch (gpt5CostError) { - logger.warn('Error in GPT-5 High cost recording (non-stream):', gpt5CostError) - // 不影响主流程 - } } // 返回响应 @@ -656,58 +487,6 @@ const handleResponses = async (req, res) => { logger.info( `📊 Recorded OpenAI usage - Input: ${inputTokens}, Output: ${outputTokens}, Total: ${usageData.total_tokens || inputTokens + outputTokens}, Model: ${modelToRecord} (actual: ${actualModel}, requested: ${requestedModel})` ) - - // 🔥 安全记录 GPT-5 High 推理级别费用 - try { - if (actualModel && String(actualModel).toLowerCase().includes('gpt-5')) { - // 安全提取推理级别 - const originalRequestBody = req.body || {} - let detectedLevel = 'medium' // 安全默认值 - - try { - detectedLevel = - originalRequestBody.reasoning_effort || - originalRequestBody.model_reasoning_effort || - 'medium' - - // 检查 reasoning 字段 - const reasoningField = originalRequestBody.reasoning - if (reasoningField) { - if (typeof reasoningField === 'string') { - if (reasoningField.includes('high') || reasoningField.includes('maximum')) { - detectedLevel = 'high' - } - } else if (typeof reasoningField === 'object' && reasoningField.effort) { - detectedLevel = reasoningField.effort - } - } - } catch (levelError) { - logger.debug('Error extracting reasoning level for cost recording:', levelError) - } - - // 如果是 High 级别,记录额外的费用 - if (String(detectedLevel).toLowerCase() === 'high') { - const gpt5HighCost = calculateGPT5HighCost(usageData) - - if (gpt5HighCost > 0) { - // 记录GPT-5 High专门的费用统计(用于周限制) - await redis.incrementWeeklyGPT5HighCost(apiKeyData.id, gpt5HighCost) - logger.info( - `💰 Recorded GPT-5 High weekly cost: $${gpt5HighCost.toFixed(4)} for key ${apiKeyData.id} (${apiKeyData.name})` - ) - - // 🔧 关键修复:同时记录到常规费用统计中 - await redis.incrementDailyCost(apiKeyData.id, gpt5HighCost) - logger.info( - `💰 Recorded GPT-5 High to daily cost: $${gpt5HighCost.toFixed(4)} for key ${apiKeyData.id} (${apiKeyData.name})` - ) - } - } - } - } catch (gpt5CostError) { - logger.warn('Error in GPT-5 High cost recording:', gpt5CostError) - // 不影响主流程 - } usageReported = true } catch (error) { logger.error('Failed to record OpenAI usage:', error) diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index a3ffe63a..3fc4b715 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -34,7 +34,6 @@ class ApiKeyService { allowedClients = [], dailyCostLimit = 0, weeklyOpusCostLimit = 0, - weeklyGPT5HighCostLimit = 0, // 新增:GPT-5 High推理级别周费用限制 tags = [], activationDays = 0, // 新增:激活后有效天数(0表示不使用此功能) expirationMode = 'fixed', // 新增:过期模式 'fixed'(固定时间) 或 'activation'(首次使用后激活) @@ -70,7 +69,6 @@ class ApiKeyService { allowedClients: JSON.stringify(allowedClients || []), dailyCostLimit: String(dailyCostLimit || 0), weeklyOpusCostLimit: String(weeklyOpusCostLimit || 0), - weeklyGPT5HighCostLimit: String(weeklyGPT5HighCostLimit || 0), // 新增:GPT-5 High周费用限制 tags: JSON.stringify(tags || []), activationDays: String(activationDays || 0), // 新增:激活后有效天数 expirationMode: expirationMode || 'fixed', // 新增:过期模式 @@ -114,7 +112,6 @@ class ApiKeyService { allowedClients: JSON.parse(keyData.allowedClients || '[]'), dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0), weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0), - weeklyGPT5HighCostLimit: parseFloat(keyData.weeklyGPT5HighCostLimit || 0), // 新增:GPT-5 High周费用限制 tags: JSON.parse(keyData.tags || '[]'), activationDays: parseInt(keyData.activationDays || 0), expirationMode: keyData.expirationMode || 'fixed', @@ -122,8 +119,7 @@ class ApiKeyService { activatedAt: keyData.activatedAt, createdAt: keyData.createdAt, expiresAt: keyData.expiresAt, - createdBy: keyData.createdBy, - icon: keyData.icon || '' // 新增:图标 + createdBy: keyData.createdBy } } @@ -416,10 +412,8 @@ class ApiKeyService { key.permissions = key.permissions || 'all' // 兼容旧数据 key.dailyCostLimit = parseFloat(key.dailyCostLimit || 0) key.weeklyOpusCostLimit = parseFloat(key.weeklyOpusCostLimit || 0) - key.weeklyGPT5HighCostLimit = parseFloat(key.weeklyGPT5HighCostLimit || 0) // 新增:GPT-5 High周费用限制 key.dailyCost = (await redis.getDailyCost(key.id)) || 0 key.weeklyOpusCost = (await redis.getWeeklyOpusCost(key.id)) || 0 - key.weeklyGPT5HighCost = (await redis.getWeeklyGPT5HighCost(key.id)) || 0 // 新增:GPT-5 High当前周费用 key.activationDays = parseInt(key.activationDays || 0) key.expirationMode = key.expirationMode || 'fixed' key.isActivated = key.isActivated === 'true' diff --git a/src/services/geminiAccountService.js b/src/services/geminiAccountService.js index f9e8eeb0..60c404f1 100644 --- a/src/services/geminiAccountService.js +++ b/src/services/geminiAccountService.js @@ -16,9 +16,9 @@ const { const tokenRefreshService = require('./tokenRefreshService') const LRUCache = require('../utils/lruCache') -// Gemini CLI OAuth 配置 - 从环境变量或配置文件读取 -const OAUTH_CLIENT_ID = process.env.GEMINI_OAUTH_CLIENT_ID || config.gemini?.oauthClientId || 'your-oauth-client-id' -const OAUTH_CLIENT_SECRET = process.env.GEMINI_OAUTH_CLIENT_SECRET || config.gemini?.oauthClientSecret || 'your-oauth-client-secret' +// Gemini CLI OAuth 配置 - 这些是公开的 Gemini CLI 凭据 +const OAUTH_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com' +const OAUTH_CLIENT_SECRET = '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' const OAUTH_SCOPES = ['https://www.googleapis.com/auth/cloud-platform'] // 加密相关常量 diff --git a/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue b/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue index 19680b51..4e78520f 100644 --- a/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue +++ b/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue @@ -110,70 +110,26 @@ class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-2 sm:text-sm" >名称 * - +
+ +

{{ errors.name }}

- -
- -
- -
-
- API Key图标 -
- -
- - -
- - -

- 支持 PNG、JPG 格式,建议尺寸 64x64px -

-
-
-
-
- -
- -
-
- - - - -
- -

- 设置 GPT-5 High推理级别的周费用限制(周一到周日),0 或留空表示无限制 -

-
-
-
import { ref, reactive, computed, onMounted } from 'vue' -import { ElMessage } from 'element-plus' import { showToast } from '@/utils/toast' import { useClientsStore } from '@/stores/clients' import { useApiKeysStore } from '@/stores/apiKeys' @@ -957,7 +862,6 @@ const form = reactive({ concurrencyLimit: '', dailyCostLimit: '', weeklyOpusCostLimit: '', - weeklyGPT5HighCostLimit: '', // 新增:GPT-5 High推理级别周费用限制 expireDuration: '', customExpireDate: '', expiresAt: null, @@ -973,8 +877,7 @@ const form = reactive({ modelInput: '', enableClientRestriction: false, allowedClients: [], - tags: [], - icon: '' // 新增:图标(base64编码) + tags: [] }) // 加载支持的客户端和已存在的标签 @@ -995,35 +898,6 @@ onMounted(async () => { } }) -// 处理图标上传 -const handleIconUpload = (event) => { - const file = event.target.files?.[0] - if (!file) return - - // 检查文件类型 - if (!file.type.startsWith('image/')) { - ElMessage.error('请选择图片文件') - return - } - - // 检查文件大小 (限制为 2MB) - if (file.size > 2 * 1024 * 1024) { - ElMessage.error('图片大小不能超过 2MB') - return - } - - // 读取文件并转换为 base64 - const reader = new FileReader() - reader.onload = (e) => { - form.icon = e.target.result - ElMessage.success('图标上传成功') - } - reader.onerror = () => { - ElMessage.error('图标上传失败') - } - reader.readAsDataURL(file) -} - // 刷新账号列表 const refreshAccounts = async () => { accountsLoading.value = true @@ -1178,22 +1052,7 @@ const removeRestrictedModel = (index) => { } // 常用模型列表 -const commonModels = ref([ - // Claude 模型 - 'claude-opus-4-20250514', - 'claude-opus-4-1-20250805', - // OpenAI 模型 - 'gpt-5', - 'gpt-5 minimal', - 'gpt-5 low', - 'gpt-5 medium', - 'gpt-5 high', - 'gpt-4o', - 'gpt-4o-mini', - 'o1', - 'o1-mini', - 'o1-preview' -]) +const commonModels = ref(['claude-opus-4-20250514', 'claude-opus-4-1-20250805']) // 可用的快捷模型(过滤掉已在限制列表中的) const availableQuickModels = computed(() => { @@ -1296,11 +1155,6 @@ const createApiKey = async () => { form.weeklyOpusCostLimit !== '' && form.weeklyOpusCostLimit !== null ? parseFloat(form.weeklyOpusCostLimit) : 0, - // 新增:GPT-5 High推理级别周费用限制 - weeklyGPT5HighCostLimit: - form.weeklyGPT5HighCostLimit !== '' && form.weeklyGPT5HighCostLimit !== null - ? parseFloat(form.weeklyGPT5HighCostLimit) - : 0, expiresAt: form.expirationMode === 'fixed' ? form.expiresAt || undefined : undefined, expirationMode: form.expirationMode, activationDays: form.expirationMode === 'activation' ? form.activationDays : undefined, @@ -1309,8 +1163,7 @@ const createApiKey = async () => { enableModelRestriction: form.enableModelRestriction, restrictedModels: form.restrictedModels, enableClientRestriction: form.enableClientRestriction, - allowedClients: form.allowedClients, - icon: form.icon || undefined // 新增:图标 + allowedClients: form.allowedClients } // 处理Claude账户绑定(区分OAuth和Console) diff --git a/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue b/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue index e0bb5844..fda19ade 100644 --- a/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue +++ b/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue @@ -32,14 +32,16 @@ class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-3 sm:text-sm" >名称 - +
+ +

用于识别此 API Key 的用途

@@ -320,56 +322,6 @@
- -
- -
-
- - - - -
- -

- 设置 GPT-5 High推理级别的周费用限制(周一到周日),0 或留空表示无限制 -

-
-
-
{ } // 常用模型列表 -const commonModels = ref([ - // Claude 模型 - 'claude-opus-4-20250514', - 'claude-opus-4-1-20250805', - // OpenAI 模型 - 'gpt-5', - 'gpt-5 minimal', - 'gpt-5 low', - 'gpt-5 medium', - 'gpt-5 high', - 'gpt-4o', - 'gpt-4o-mini', - 'o1', - 'o1-mini', - 'o1-preview' -]) +const commonModels = ref(['claude-opus-4-20250514', 'claude-opus-4-1-20250805']) // 可用的快捷模型(过滤掉已在限制列表中的) const availableQuickModels = computed(() => { @@ -894,11 +830,6 @@ const updateApiKey = async () => { form.weeklyOpusCostLimit !== '' && form.weeklyOpusCostLimit !== null ? parseFloat(form.weeklyOpusCostLimit) : 0, - // 新增:GPT-5 High推理级别周费用限制 - weeklyGPT5HighCostLimit: - form.weeklyGPT5HighCostLimit !== '' && form.weeklyGPT5HighCostLimit !== null - ? parseFloat(form.weeklyGPT5HighCostLimit) - : 0, permissions: form.permissions, tags: form.tags } @@ -1122,7 +1053,6 @@ onMounted(async () => { form.concurrencyLimit = props.apiKey.concurrencyLimit || '' form.dailyCostLimit = props.apiKey.dailyCostLimit || '' form.weeklyOpusCostLimit = props.apiKey.weeklyOpusCostLimit || '' - form.weeklyGPT5HighCostLimit = props.apiKey.weeklyGPT5HighCostLimit || '' // 新增 form.permissions = props.apiKey.permissions || 'all' // 处理 Claude 账号(区分 OAuth 和 Console) if (props.apiKey.claudeConsoleAccountId) { diff --git a/web/admin-spa/src/components/apikeys/LimitProgressBar.vue b/web/admin-spa/src/components/apikeys/LimitProgressBar.vue index 6e0181ae..a11e582f 100644 --- a/web/admin-spa/src/components/apikeys/LimitProgressBar.vue +++ b/web/admin-spa/src/components/apikeys/LimitProgressBar.vue @@ -1,6 +1,6 @@