diff --git a/src/routes/geminiRoutes.js b/src/routes/geminiRoutes.js index 8ece60fd..78a44bd3 100644 --- a/src/routes/geminiRoutes.js +++ b/src/routes/geminiRoutes.js @@ -349,6 +349,65 @@ router.get('/key-info', authenticateApiKey, async (req, res) => { } }) +// 通用的简单端点处理函数(用于直接转发的端点) +// 适用于:listExperiments 等不需要特殊业务逻辑的端点 +async function handleSimpleEndpoint(apiMethod) { + return async (req, res) => { + try { + if (!ensureGeminiPermission(req, res)) { + return undefined + } + + const sessionHash = sessionHelper.generateSessionHash(req.body) + + // 从路径参数或请求体中获取模型名 + const requestedModel = req.body.model || req.params.modelName || 'gemini-2.5-flash' + const { accountId } = await unifiedGeminiScheduler.selectAccountForApiKey( + req.apiKey, + sessionHash, + requestedModel + ) + const account = await geminiAccountService.getAccount(accountId) + const { accessToken, refreshToken } = account + + const version = req.path.includes('v1beta') ? 'v1beta' : 'v1internal' + logger.info(`${apiMethod} request (${version})`, { + apiKeyId: req.apiKey?.id || 'unknown', + requestBody: req.body + }) + + // 解析账户的代理配置 + let proxyConfig = null + if (account.proxy) { + try { + proxyConfig = typeof account.proxy === 'string' ? JSON.parse(account.proxy) : account.proxy + } catch (e) { + logger.warn('Failed to parse proxy configuration:', e) + } + } + + const client = await geminiAccountService.getOauthClient(accessToken, refreshToken, proxyConfig) + + // 直接转发请求体,不做特殊处理 + const response = await geminiAccountService.forwardToCodeAssist( + client, + apiMethod, + req.body, + proxyConfig + ) + + res.json(response) + } catch (error) { + const version = req.path.includes('v1beta') ? 'v1beta' : 'v1internal' + logger.error(`Error in ${apiMethod} endpoint (${version})`, { error: error.message }) + res.status(500).json({ + error: 'Internal server error', + message: error.message + }) + } + } +} + // 共用的 loadCodeAssist 处理函数 async function handleLoadCodeAssist(req, res) { try { @@ -1040,6 +1099,7 @@ router.post('/v1internal\\:onboardUser', authenticateApiKey, handleOnboardUser) router.post('/v1internal\\:countTokens', authenticateApiKey, handleCountTokens) router.post('/v1internal\\:generateContent', authenticateApiKey, handleGenerateContent) router.post('/v1internal\\:streamGenerateContent', authenticateApiKey, handleStreamGenerateContent) +router.post('/v1internal\\:listExperiments', authenticateApiKey, handleSimpleEndpoint('listExperiments')) // v1beta 版本的端点 - 支持动态模型名称 router.post('/v1beta/models/:modelName\\:loadCodeAssist', authenticateApiKey, handleLoadCodeAssist) @@ -1055,6 +1115,11 @@ router.post( authenticateApiKey, handleStreamGenerateContent ) +router.post( + '/v1beta/models/:modelName\\:listExperiments', + authenticateApiKey, + handleSimpleEndpoint('listExperiments') +) // 导出处理函数供标准路由使用 module.exports = router diff --git a/src/services/geminiAccountService.js b/src/services/geminiAccountService.js index 2e35966e..e50d1744 100644 --- a/src/services/geminiAccountService.js +++ b/src/services/geminiAccountService.js @@ -1060,6 +1060,47 @@ async function getOauthClient(accessToken, refreshToken, proxyConfig = null) { return client } +// 通用的 Code Assist API 转发函数(用于简单的请求/响应端点) +// 适用于:loadCodeAssist, onboardUser, countTokens, listExperiments 等不需要特殊处理的端点 +async function forwardToCodeAssist(client, apiMethod, requestBody, proxyConfig = null) { + const axios = require('axios') + const CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com' + const CODE_ASSIST_API_VERSION = 'v1internal' + + const { token } = await client.getAccessToken() + const proxyAgent = ProxyHelper.createProxyAgent(proxyConfig) + + logger.info(`📡 ${apiMethod} API调用开始`) + + const axiosConfig = { + url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:${apiMethod}`, + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + data: requestBody, + timeout: 30000 + } + + // 添加代理配置 + if (proxyAgent) { + axiosConfig.httpAgent = proxyAgent + axiosConfig.httpsAgent = proxyAgent + axiosConfig.proxy = false + logger.info( + `🌐 Using proxy for ${apiMethod}: ${ProxyHelper.getProxyDescription(proxyConfig)}` + ) + } else { + logger.debug(`🌐 No proxy configured for ${apiMethod}`) + } + + const response = await axios(axiosConfig) + + logger.info(`✅ ${apiMethod} API调用成功`) + return response.data +} + // 调用 Google Code Assist API 的 loadCodeAssist 方法(支持代理) async function loadCodeAssist(client, projectId = null, proxyConfig = null) { const axios = require('axios') @@ -1069,54 +1110,10 @@ async function loadCodeAssist(client, projectId = null, proxyConfig = null) { const { token } = await client.getAccessToken() const proxyAgent = ProxyHelper.createProxyAgent(proxyConfig) - const tokenInfoConfig = { - url: 'https://oauth2.googleapis.com/tokeninfo', - method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/x-www-form-urlencoded' - }, - data: new URLSearchParams({ access_token: token }).toString(), - timeout: 15000 - } - - if (proxyAgent) { - tokenInfoConfig.httpAgent = proxyAgent - tokenInfoConfig.httpsAgent = proxyAgent - tokenInfoConfig.proxy = false - } - - try { - await axios(tokenInfoConfig) - logger.info('📋 tokeninfo 接口验证成功') - } catch (error) { - logger.info('tokeninfo 接口获取失败', error) - } - - const userInfoConfig = { - url: 'https://www.googleapis.com/oauth2/v2/userinfo', - method: 'GET', - headers: { - Authorization: `Bearer ${token}`, - Accept: '*/*' - }, - timeout: 15000 - } - - if (proxyAgent) { - userInfoConfig.httpAgent = proxyAgent - userInfoConfig.httpsAgent = proxyAgent - userInfoConfig.proxy = false - } - - try { - await axios(userInfoConfig) - logger.info('📋 userinfo 接口获取成功') - } catch (error) { - logger.info('userinfo 接口获取失败', error) - } - // 创建ClientMetadata + // Note: 移除了 tokeninfo 和 userinfo 验证调用 + // 这些调用在原生 gemini-cli 的 CodeAssistServer.loadCodeAssist 中不存在 + // 且会导致使用特殊 token 时出现 401 错误 const clientMetadata = { ideType: 'IDE_UNSPECIFIED', platform: 'PLATFORM_UNSPECIFIED', @@ -1573,6 +1570,7 @@ module.exports = { getAccountRateLimitInfo, isTokenExpired, getOauthClient, + forwardToCodeAssist, // 通用转发函数 loadCodeAssist, getOnboardTier, onboardUser,