From 530dac0e7fa4dc41e3570aecb3287315499be83c Mon Sep 17 00:00:00 2001 From: jft0m <377632523@qq.com> Date: Sat, 4 Oct 2025 14:11:13 +0800 Subject: [PATCH 1/5] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=20handleChat?= =?UTF-8?q?Completions=20=E5=87=BD=E6=95=B0=E6=A8=A1=E5=9D=97=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用策略模式处理不同后端(Claude/OpenAI/Gemini) - 添加 OpenAI chat/completions 兼容支持 --- src/routes/api.js | 500 ++++++++++++++++++++++++++++++++- src/services/openaiToClaude.js | 19 +- 2 files changed, 507 insertions(+), 12 deletions(-) diff --git a/src/routes/api.js b/src/routes/api.js index d4b572d4..825aeff1 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -12,9 +12,28 @@ const logger = require('../utils/logger') const redis = require('../models/redis') const { getEffectiveModel, parseVendorPrefixedModel } = require('../utils/modelHelper') const sessionHelper = require('../utils/sessionHelper') +const openaiToClaude = require('../services/openaiToClaude') +const claudeCodeHeadersService = require('../services/claudeCodeHeadersService') const router = express.Router() +// 🔍 检测模型对应的后端服务 +function detectBackendFromModel(modelName) { + if (!modelName) { + return 'claude' + } + if (modelName.startsWith('claude-')) { + return 'claude' + } + if (modelName.startsWith('gpt-')) { + return 'openai' + } + if (modelName.startsWith('gemini-')) { + return 'gemini' + } + return 'claude' // 默认使用 Claude +} + // 🔧 共享的消息处理函数 async function handleMessagesRequest(req, res) { try { @@ -778,25 +797,20 @@ router.post('/v1/messages', authenticateApiKey, handleMessagesRequest) // 🚀 Claude API messages 端点 - /claude/v1/messages (别名) router.post('/claude/v1/messages', authenticateApiKey, handleMessagesRequest) -// 📋 模型列表端点 - Claude Code 客户端需要 +// 📋 模型列表端点 - OpenAI 兼容,返回所有支持的模型 router.get('/v1/models', authenticateApiKey, async (req, res) => { try { - // 返回支持的模型列表 + // 返回支持的模型列表(Claude + OpenAI + Gemini) const models = [ + // Claude 模型 { - id: 'claude-3-5-sonnet-20241022', + id: 'claude-sonnet-4-5-20250929', object: 'model', created: 1669599635, owned_by: 'anthropic' }, { - id: 'claude-3-5-haiku-20241022', - object: 'model', - created: 1669599635, - owned_by: 'anthropic' - }, - { - id: 'claude-3-opus-20240229', + id: 'claude-opus-4-1-20250805', object: 'model', created: 1669599635, owned_by: 'anthropic' @@ -806,6 +820,92 @@ router.get('/v1/models', authenticateApiKey, async (req, res) => { object: 'model', created: 1669599635, owned_by: 'anthropic' + }, + { + id: 'claude-opus-4-20250514', + object: 'model', + created: 1669599635, + owned_by: 'anthropic' + }, + { + id: 'claude-3-7-sonnet-20250219', + object: 'model', + created: 1669599635, + owned_by: 'anthropic' + }, + { + id: 'claude-3-5-sonnet-20241022', + object: 'model', + created: 1729036800, + owned_by: 'anthropic' + }, + { + id: 'claude-3-5-haiku-20241022', + object: 'model', + created: 1729036800, + owned_by: 'anthropic' + }, + { + id: 'claude-3-haiku-20240307', + object: 'model', + created: 1709251200, + owned_by: 'anthropic' + }, + { + id: 'claude-3-opus-20240229', + object: 'model', + created: 1736726400, + owned_by: 'anthropic' + }, + // OpenAI 模型 + { + id: 'gpt-4o', + object: 'model', + created: 1715367600, + owned_by: 'openai' + }, + { + id: 'gpt-4o-mini', + object: 'model', + created: 1721088000, + owned_by: 'openai' + }, + { + id: 'gpt-4-turbo', + object: 'model', + created: 1712102400, + owned_by: 'openai' + }, + { + id: 'gpt-4', + object: 'model', + created: 1687132800, + owned_by: 'openai' + }, + { + id: 'gpt-3.5-turbo', + object: 'model', + created: 1677649200, + owned_by: 'openai' + }, + // Gemini 模型 + { + id: 'gemini-1.5-pro', + object: 'model', + created: 1707868800, + owned_by: 'google' + }, + { + id: 'gemini-1.5-flash', + object: 'model', + created: 1715990400, + owned_by: 'google' + }, + { + id: 'gemini-2.0-flash-exp', + object: 'model', + created: 1733011200, + owned_by: 'google' } ] @@ -1038,5 +1138,385 @@ router.post('/v1/messages/count_tokens', authenticateApiKey, async (req, res) => } }) +// 🔍 验证 OpenAI chat/completions 请求参数 +function validateChatCompletionRequest(body) { + if (!body || !body.messages || !Array.isArray(body.messages)) { + return { + valid: false, + error: { + message: 'Missing or invalid field: messages (must be an array)', + type: 'invalid_request_error', + code: 'invalid_request' + } + } + } + + if (body.messages.length === 0) { + return { + valid: false, + error: { + message: 'Messages array cannot be empty', + type: 'invalid_request_error', + code: 'invalid_request' + } + } + } + + return { valid: true } +} + +// 🌊 处理 Claude 流式请求 +async function handleClaudeStreamRequest( + claudeRequest, + apiKeyData, + req, + res, + accountId, + requestedModel +) { + logger.info(`🌊 Processing OpenAI stream request for model: ${requestedModel}`) + + // 设置 SSE 响应头 + res.setHeader('Content-Type', 'text/event-stream') + res.setHeader('Cache-Control', 'no-cache') + res.setHeader('Connection', 'keep-alive') + res.setHeader('X-Accel-Buffering', 'no') + + // 创建中止控制器 + const abortController = new AbortController() + + // 处理客户端断开 + req.on('close', () => { + if (abortController && !abortController.signal.aborted) { + logger.info('🔌 Client disconnected, aborting Claude request') + abortController.abort() + } + }) + + // 获取该账号存储的 Claude Code headers + const claudeCodeHeaders = await claudeCodeHeadersService.getAccountHeaders(accountId) + + // 使用转换后的响应流 + await claudeRelayService.relayStreamRequestWithUsageCapture( + claudeRequest, + apiKeyData, + res, + claudeCodeHeaders, + (usage) => { + // 记录使用统计 + if (usage && usage.input_tokens !== undefined && usage.output_tokens !== undefined) { + const model = usage.model || claudeRequest.model + + apiKeyService + .recordUsageWithDetails(apiKeyData.id, usage, model, accountId) + .catch((error) => { + logger.error('❌ Failed to record usage:', error) + }) + } + }, + // 流转换器:将 Claude SSE 转换为 OpenAI SSE + (() => { + const sessionId = `chatcmpl-${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}` + return (chunk) => openaiToClaude.convertStreamChunk(chunk, requestedModel, sessionId) + })(), + { + betaHeader: + 'oauth-2025-04-20,claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14' + } + ) + + return { abortController } +} + +// 📄 处理 Claude 非流式请求 +async function handleClaudeNonStreamRequest( + claudeRequest, + apiKeyData, + req, + res, + accountId, + requestedModel +) { + logger.info(`📄 Processing OpenAI non-stream request for model: ${requestedModel}`) + + // 获取该账号存储的 Claude Code headers + const claudeCodeHeaders = await claudeCodeHeadersService.getAccountHeaders(accountId) + + // 发送请求到 Claude + const claudeResponse = await claudeRelayService.relayRequest( + claudeRequest, + apiKeyData, + req, + res, + claudeCodeHeaders, + { betaHeader: 'oauth-2025-04-20' } + ) + + // 解析 Claude 响应 + let claudeData + try { + claudeData = JSON.parse(claudeResponse.body) + } catch (error) { + logger.error('❌ Failed to parse Claude response:', error) + return { + error: { + status: 502, + data: { + error: { + message: 'Invalid response from Claude API', + type: 'api_error', + code: 'invalid_response' + } + } + } + } + } + + // 处理错误响应 + if (claudeResponse.statusCode >= 400) { + return { + error: { + status: claudeResponse.statusCode, + data: { + error: { + message: claudeData.error?.message || 'Claude API error', + type: claudeData.error?.type || 'api_error', + code: claudeData.error?.code || 'unknown_error' + } + } + } + } + } + + // 转换为 OpenAI 格式 + const openaiResponse = openaiToClaude.convertResponse(claudeData, requestedModel) + + // 记录使用统计 + if (claudeData.usage) { + const { usage } = claudeData + apiKeyService + .recordUsageWithDetails(apiKeyData.id, usage, claudeRequest.model, accountId) + .catch((error) => { + logger.error('❌ Failed to record usage:', error) + }) + } + + return { success: true, data: openaiResponse } +} + +// 🤖 处理 Claude 后端 +async function handleClaudeBackend(req, res, apiKeyData, requestedModel) { + // 转换 OpenAI 请求为 Claude 格式 + const claudeRequest = openaiToClaude.convertRequest(req.body) + + // 检查模型限制 + if (apiKeyData.enableModelRestriction && apiKeyData.restrictedModels?.length > 0) { + if (!apiKeyData.restrictedModels.includes(claudeRequest.model)) { + return res.status(403).json({ + error: { + message: `Model ${requestedModel} is not allowed for this API key`, + type: 'invalid_request_error', + code: 'model_not_allowed' + } + }) + } + } + + // 生成会话哈希用于 sticky 会话 + const sessionHash = sessionHelper.generateSessionHash(claudeRequest) + + // 选择可用的 Claude 账户 + const accountSelection = await unifiedClaudeScheduler.selectAccountForApiKey( + apiKeyData, + sessionHash, + claudeRequest.model + ) + const { accountId } = accountSelection + + // 处理流式或非流式请求 + if (claudeRequest.stream) { + const { abortController } = await handleClaudeStreamRequest( + claudeRequest, + apiKeyData, + req, + res, + accountId, + requestedModel + ) + return { abortController } + } else { + const result = await handleClaudeNonStreamRequest( + claudeRequest, + apiKeyData, + req, + res, + accountId, + requestedModel + ) + + if (result.error) { + return res.status(result.error.status).json(result.error.data) + } + + return res.json(result.data) + } +} + +// 🔧 处理 OpenAI 后端(未实现) +async function handleOpenAIBackend(req, res, _apiKeyData, _requestedModel) { + return res.status(501).json({ + error: { + message: 'OpenAI backend not yet implemented for this endpoint', + type: 'not_implemented', + code: 'not_implemented' + } + }) +} + +// 💎 处理 Gemini 后端(未实现) +async function handleGeminiBackend(req, res, _apiKeyData, _requestedModel) { + return res.status(501).json({ + error: { + message: 'Gemini backend not yet implemented for this endpoint', + type: 'not_implemented', + code: 'not_implemented' + } + }) +} + +// 🗺️ 后端处理策略映射 +const backendHandlers = { + claude: handleClaudeBackend, + openai: handleOpenAIBackend, + gemini: handleGeminiBackend +} + +// 🚀 OpenAI 兼容的 chat/completions 处理器(智能路由) +async function handleChatCompletions(req, res) { + const startTime = Date.now() + let abortController = null + + try { + const apiKeyData = req.apiKey + + // 验证必需参数 + const validation = validateChatCompletionRequest(req.body) + if (!validation.valid) { + return res.status(400).json({ error: validation.error }) + } + + // 检测模型对应的后端 + const requestedModel = req.body.model || 'claude-3-5-sonnet-20241022' + const backend = detectBackendFromModel(requestedModel) + + logger.debug( + `📥 Received OpenAI format request for model: ${requestedModel}, backend: ${backend}` + ) + + // 使用策略模式处理不同后端 + const handler = backendHandlers[backend] + if (!handler) { + return res.status(500).json({ + error: { + message: `Unsupported backend: ${backend}`, + type: 'server_error', + code: 'unsupported_backend' + } + }) + } + + // 调用对应的后端处理器 + const result = await handler(req, res, apiKeyData, requestedModel) + + // 保存 abort controller(用于清理) + if (result && result.abortController) { + ;({ abortController } = result) + } + + const duration = Date.now() - startTime + logger.info(`✅ OpenAI chat/completions request completed in ${duration}ms`) + return undefined + } catch (error) { + logger.error('❌ OpenAI chat/completions error:', error) + + const status = error.status || 500 + if (!res.headersSent) { + res.status(status).json({ + error: { + message: error.message || 'Internal server error', + type: 'server_error', + code: 'internal_error' + } + }) + } + return undefined + } finally { + // 清理资源 + if (abortController) { + abortController = null + } + } +} + +// 🔧 OpenAI 兼容的 completions 处理器(传统格式,转换为 chat 格式) +async function handleCompletions(req, res) { + try { + // 验证必需参数 + if (!req.body.prompt) { + return res.status(400).json({ + error: { + message: 'Prompt is required', + type: 'invalid_request_error', + code: 'invalid_request' + } + }) + } + + // 将传统 completions 格式转换为 chat 格式 + const chatRequest = { + model: req.body.model || 'claude-3-5-sonnet-20241022', + messages: [ + { + role: 'user', + content: req.body.prompt + } + ], + max_tokens: req.body.max_tokens, + temperature: req.body.temperature, + top_p: req.body.top_p, + stream: req.body.stream, + stop: req.body.stop, + n: req.body.n || 1, + presence_penalty: req.body.presence_penalty, + frequency_penalty: req.body.frequency_penalty, + logit_bias: req.body.logit_bias, + user: req.body.user + } + + // 使用 chat/completions 处理器 + req.body = chatRequest + await handleChatCompletions(req, res) + return undefined + } catch (error) { + logger.error('❌ OpenAI completions error:', error) + if (!res.headersSent) { + res.status(500).json({ + error: { + message: 'Failed to process completion request', + type: 'server_error', + code: 'internal_error' + } + }) + } + return undefined + } +} + +// 📋 OpenAI 兼容的 chat/completions 端点 +router.post('/v1/chat/completions', authenticateApiKey, handleChatCompletions) + +// 🔧 OpenAI 兼容的 completions 端点(传统格式) +router.post('/v1/completions', authenticateApiKey, handleCompletions) + module.exports = router module.exports.handleMessagesRequest = handleMessagesRequest diff --git a/src/services/openaiToClaude.js b/src/services/openaiToClaude.js index 10c8ae24..1f335f0e 100644 --- a/src/services/openaiToClaude.js +++ b/src/services/openaiToClaude.js @@ -31,10 +31,25 @@ class OpenAIToClaudeConverter { stream: openaiRequest.stream || false } - // Claude Code 必需的系统消息 + // 定义 Claude Code 的默认系统提示词 const claudeCodeSystemMessage = "You are Claude Code, Anthropic's official CLI for Claude." - claudeRequest.system = claudeCodeSystemMessage + // 如果 OpenAI 请求中包含系统消息,提取并检查 + const systemMessage = this._extractSystemMessage(openaiRequest.messages) + if (systemMessage && systemMessage.includes('You are currently in Xcode')) { + // Xcode 系统提示词 + claudeRequest.system = systemMessage + logger.info( + `🔍 Xcode request detected, using Xcode system prompt (${systemMessage.length} chars)` + ) + logger.debug(`📋 System prompt preview: ${systemMessage.substring(0, 150)}...`) + } else { + // 使用 Claude Code 默认系统提示词 + claudeRequest.system = claudeCodeSystemMessage + logger.debug( + `📋 Using Claude Code default system prompt${systemMessage ? ' (ignored custom prompt)' : ''}` + ) + } // 处理停止序列 if (openaiRequest.stop) { From 61e5cb4584404c1181122df0c711f6a599240267 Mon Sep 17 00:00:00 2001 From: jft0m <377632523@qq.com> Date: Sat, 4 Oct 2025 13:48:07 +0800 Subject: [PATCH 2/5] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=20handleChat?= =?UTF-8?q?Completions=20=E5=87=BD=E6=95=B0=E6=A8=A1=E5=9D=97=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用策略模式处理不同后端(Claude/OpenAI/Gemini) - 添加 OpenAI chat/completions 兼容支持 - 修复代码缩进符合 ESLint 规范 --- src/validators/clients/codexCliValidator.js | 3 ++- src/validators/clients/geminiCliValidator.js | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/validators/clients/codexCliValidator.js b/src/validators/clients/codexCliValidator.js index aff09fbf..78c2a1aa 100644 --- a/src/validators/clients/codexCliValidator.js +++ b/src/validators/clients/codexCliValidator.js @@ -53,7 +53,8 @@ class CodexCliValidator { // 2. 对于特定路径,进行额外的严格验证 // 对于 /openai 和 /azure 路径需要完整验证 const strictValidationPaths = ['/openai', '/azure'] - const needsStrictValidation = req.path && strictValidationPaths.some(path => req.path.startsWith(path)) + const needsStrictValidation = + req.path && strictValidationPaths.some((path) => req.path.startsWith(path)) if (!needsStrictValidation) { // 其他路径,只要 User-Agent 匹配就认为是 Codex CLI diff --git a/src/validators/clients/geminiCliValidator.js b/src/validators/clients/geminiCliValidator.js index ea8e60e7..8e9ed0de 100644 --- a/src/validators/clients/geminiCliValidator.js +++ b/src/validators/clients/geminiCliValidator.js @@ -55,7 +55,9 @@ class GeminiCliValidator { // 包含 generateContent 的路径需要验证 User-Agent const geminiCliPattern = /^GeminiCLI\/v?[\d\.]+/i if (!geminiCliPattern.test(userAgent)) { - logger.debug(`Gemini CLI validation failed - UA mismatch for generateContent: ${userAgent}`) + logger.debug( + `Gemini CLI validation failed - UA mismatch for generateContent: ${userAgent}` + ) return false } } From 5d7225b2eb8475489213868fc22470e3d1bed2f8 Mon Sep 17 00:00:00 2001 From: jft0m <377632523@qq.com> Date: Mon, 6 Oct 2025 15:34:13 +0000 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Opus=20?= =?UTF-8?q?=E9=99=90=E6=B5=81=E7=8A=B6=E6=80=81=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在账户列表中显示 Opus 限流状态徽章 - 显示限流剩余时间(天/小时) - 后端 API 添加 opusRateLimitedAt 和 opusRateLimitEndAt 字段 - 优化徽章样式,防止文字溢出 --- src/services/claudeAccountService.js | 5 +- web/admin-spa/src/views/AccountsView.vue | 60 +++++++++++++++++++++++- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index ec9e5b11..f931345e 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -528,7 +528,10 @@ class ClaudeAccountService { useUnifiedClientId: account.useUnifiedClientId === 'true', // 默认为false unifiedClientId: account.unifiedClientId || '', // 统一的客户端标识 // 添加停止原因 - stoppedReason: account.stoppedReason || null + stoppedReason: account.stoppedReason || null, + // 添加 Opus 限流信息 + opusRateLimitedAt: account.opusRateLimitedAt || null, + opusRateLimitEndAt: account.opusRateLimitEndAt || null } }) ) diff --git a/web/admin-spa/src/views/AccountsView.vue b/web/admin-spa/src/views/AccountsView.vue index f804f639..1a9469ee 100644 --- a/web/admin-spa/src/views/AccountsView.vue +++ b/web/admin-spa/src/views/AccountsView.vue @@ -606,9 +606,19 @@ >({{ formatRateLimitTime(account.rateLimitStatus.minutesRemaining) }}) + + + Opus限流 + + ({{ formatOpusLimitEndTime(account.opusRateLimitEndAt) }}) + + 不可调度 @@ -2491,6 +2501,54 @@ const formatRateLimitTime = (minutes) => { } } +// 检查账户是否处于 Opus 限流状态 +const isOpusRateLimited = (account) => { + if (!account.opusRateLimitEndAt) { + return false + } + const endTime = new Date(account.opusRateLimitEndAt) + const now = new Date() + return endTime > now +} + +// 格式化 Opus 限流结束时间 +const formatOpusLimitEndTime = (endTimeStr) => { + if (!endTimeStr) return '' + + const endTime = new Date(endTimeStr) + const now = new Date() + + // 如果已经过期,返回"已解除" + if (endTime <= now) { + return '已解除' + } + + // 计算剩余时间(毫秒) + const remainingMs = endTime - now + const remainingMinutes = Math.floor(remainingMs / (1000 * 60)) + + // 计算天数、小时和分钟 + const days = Math.floor(remainingMinutes / 1440) + const remainingAfterDays = remainingMinutes % 1440 + const hours = Math.floor(remainingAfterDays / 60) + const mins = remainingAfterDays % 60 + + // 格式化显示 + const parts = [] + if (days > 0) { + parts.push(`${days}天`) + } + if (hours > 0) { + parts.push(`${hours}小时`) + } + if (mins > 0 && days === 0) { + // 只有在天数为0时才显示分钟 + parts.push(`${mins}分钟`) + } + + return parts.join('') +} + // 打开创建账户模态框 const openCreateAccountModal = () => { newAccountPlatform.value = null // 重置选择的平台 From bb95c0b7d566fed33cf02f286f647bcaf564755a Mon Sep 17 00:00:00 2001 From: jft0m <377632523@qq.com> Date: Mon, 6 Oct 2025 15:57:09 +0000 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20ESLint=20?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=A7=84=E8=8C=83=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除正则表达式中不必要的转义字符 - 添加 if 语句的花括号 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/validators/clients/codexCliValidator.js | 10 +++++++--- src/validators/clients/geminiCliValidator.js | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/validators/clients/codexCliValidator.js b/src/validators/clients/codexCliValidator.js index 78c2a1aa..d8922bd2 100644 --- a/src/validators/clients/codexCliValidator.js +++ b/src/validators/clients/codexCliValidator.js @@ -42,7 +42,7 @@ class CodexCliValidator { // Codex CLI 的 UA 格式: // - codex_vscode/0.35.0 (Windows 10.0.26100; x86_64) unknown (Cursor; 0.4.10) // - codex_cli_rs/0.38.0 (Ubuntu 22.4.0; x86_64) WindowsTerminal - const codexCliPattern = /^(codex_vscode|codex_cli_rs)\/[\d\.]+/i + const codexCliPattern = /^(codex_vscode|codex_cli_rs)\/[\d.]+/i const uaMatch = userAgent.match(codexCliPattern) if (!uaMatch) { @@ -125,8 +125,12 @@ class CodexCliValidator { const part1 = parts1[i] || 0 const part2 = parts2[i] || 0 - if (part1 < part2) return -1 - if (part1 > part2) return 1 + if (part1 < part2) { + return -1 + } + if (part1 > part2) { + return 1 + } } return 0 diff --git a/src/validators/clients/geminiCliValidator.js b/src/validators/clients/geminiCliValidator.js index 8e9ed0de..0d438384 100644 --- a/src/validators/clients/geminiCliValidator.js +++ b/src/validators/clients/geminiCliValidator.js @@ -53,7 +53,7 @@ class GeminiCliValidator { // 2. 对于 /gemini 路径,检查是否包含 generateContent if (path.includes('generateContent')) { // 包含 generateContent 的路径需要验证 User-Agent - const geminiCliPattern = /^GeminiCLI\/v?[\d\.]+/i + const geminiCliPattern = /^GeminiCLI\/v?[\d.]+/i if (!geminiCliPattern.test(userAgent)) { logger.debug( `Gemini CLI validation failed - UA mismatch for generateContent: ${userAgent}` @@ -84,8 +84,12 @@ class GeminiCliValidator { const part1 = parts1[i] || 0 const part2 = parts2[i] || 0 - if (part1 < part2) return -1 - if (part1 > part2) return 1 + if (part1 < part2) { + return -1 + } + if (part1 > part2) { + return 1 + } } return 0 From 8f9286c30eb5957f336c0afc89a522bbac486aeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=BC=E6=9E=97=E6=B6=9B?= Date: Wed, 8 Oct 2025 19:49:36 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20ESLint=20?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=A7=84=E8=8C=83=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/api.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/routes/api.js b/src/routes/api.js index 42858c52..7d700c9d 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -34,8 +34,6 @@ function detectBackendFromModel(modelName) { const { updateRateLimitCounters } = require('../utils/rateLimitHelper') -const router = express.Router() - function queueRateLimitUpdate(rateLimitInfo, usageSummary, model, context = '') { if (!rateLimitInfo) { return Promise.resolve({ totalTokens: 0, totalCost: 0 })