From 9ed4a344be759b55baec63cac62a8875e8738238 Mon Sep 17 00:00:00 2001 From: shaw Date: Fri, 17 Oct 2025 16:11:12 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dgemini=E8=BD=AC?= =?UTF-8?q?=E5=8F=91=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/admin.js | 8 +- src/routes/standardGeminiRoutes.js | 109 ++++++++++++++++++++++++++- src/services/geminiAccountService.js | 38 +++++++++- 3 files changed, 146 insertions(+), 9 deletions(-) diff --git a/src/routes/admin.js b/src/routes/admin.js index 0206c6c1..e9034acf 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -3624,8 +3624,8 @@ router.post('/bedrock-accounts', authenticateAdmin, async (req, res) => { } logger.success(`☁️ Admin created Bedrock account: ${name}`) - const formattedAccount = formatAccountExpiry(formattedAccount) - return res.json({ success: true, data: result.data }) + const formattedAccount = formatAccountExpiry(result.data) + return res.json({ success: true, data: formattedAccount }) } catch (error) { logger.error('❌ Failed to create Bedrock account:', error) return res @@ -4078,8 +4078,8 @@ router.post('/gemini-accounts', authenticateAdmin, async (req, res) => { } logger.success(`🏢 Admin created new Gemini account: ${accountData.name}`) - const formattedAccount = formatAccountExpiry(formattedAccount) - return res.json({ success: true, data: newAccount }) + const formattedAccount = formatAccountExpiry(newAccount) + return res.json({ success: true, data: formattedAccount }) } catch (error) { logger.error('❌ Failed to create Gemini account:', error) return res.status(500).json({ error: 'Failed to create account', message: error.message }) diff --git a/src/routes/standardGeminiRoutes.js b/src/routes/standardGeminiRoutes.js index afcb98dc..57c5b3a3 100644 --- a/src/routes/standardGeminiRoutes.js +++ b/src/routes/standardGeminiRoutes.js @@ -44,6 +44,90 @@ function ensureGeminiPermissionMiddleware(req, res, next) { return undefined } +// 判断对象是否为可读流 +function isReadableStream(value) { + return value && typeof value.on === 'function' && typeof value.pipe === 'function' +} + +// 读取可读流内容为字符串 +async function readStreamToString(stream) { + return new Promise((resolve, reject) => { + let result = '' + + try { + if (typeof stream.setEncoding === 'function') { + stream.setEncoding('utf8') + } + } catch (error) { + logger.warn('设置流编码失败:', error) + } + + stream.on('data', (chunk) => { + result += chunk + }) + + stream.on('end', () => { + resolve(result) + }) + + stream.on('error', (error) => { + reject(error) + }) + }) +} + +// 规范化上游 Axios 错误信息 +async function normalizeAxiosStreamError(error) { + const status = error.response?.status + const statusText = error.response?.statusText + const responseData = error.response?.data + let rawBody = null + let parsedBody = null + + if (responseData) { + try { + if (isReadableStream(responseData)) { + rawBody = await readStreamToString(responseData) + } else if (Buffer.isBuffer(responseData)) { + rawBody = responseData.toString('utf8') + } else if (typeof responseData === 'string') { + rawBody = responseData + } else { + rawBody = JSON.stringify(responseData) + } + } catch (streamError) { + logger.warn('读取 Gemini 上游错误流失败:', streamError) + } + } + + if (rawBody) { + if (typeof rawBody === 'string') { + try { + parsedBody = JSON.parse(rawBody) + } catch (parseError) { + parsedBody = rawBody + } + } else { + parsedBody = rawBody + } + } + + let finalMessage = error.message || 'Internal server error' + if (parsedBody && typeof parsedBody === 'object') { + finalMessage = parsedBody.error?.message || parsedBody.message || finalMessage + } else if (typeof parsedBody === 'string' && parsedBody.trim()) { + finalMessage = parsedBody.trim() + } + + return { + status, + statusText, + message: finalMessage, + parsedBody, + rawBody + } +} + // 标准 Gemini API 路由处理器 // 这些路由将挂载在 /gemini 路径下,处理标准 Gemini API 格式的请求 // 标准格式: /gemini/v1beta/models/{model}:generateContent @@ -552,21 +636,38 @@ async function handleStandardStreamGenerateContent(req, res) { } }) } catch (error) { + const normalizedError = await normalizeAxiosStreamError(error) + logger.error(`Error in standard streamGenerateContent endpoint`, { message: error.message, status: error.response?.status, statusText: error.response?.statusText, - responseData: error.response?.data, + responseData: normalizedError.parsedBody || normalizedError.rawBody, stack: error.stack }) if (!res.headersSent) { - res.status(500).json({ + const statusCode = normalizedError.status || 500 + const responseBody = { error: { - message: error.message || 'Internal server error', + message: normalizedError.message, type: 'api_error' } - }) + } + + if (normalizedError.status) { + responseBody.error.upstreamStatus = normalizedError.status + } + if (normalizedError.statusText) { + responseBody.error.upstreamStatusText = normalizedError.statusText + } + if (normalizedError.parsedBody && typeof normalizedError.parsedBody === 'object') { + responseBody.error.upstreamResponse = normalizedError.parsedBody + } else if (normalizedError.rawBody) { + responseBody.error.upstreamRaw = normalizedError.rawBody + } + + return res.status(statusCode).json(responseBody) } } finally { // 清理资源 diff --git a/src/services/geminiAccountService.js b/src/services/geminiAccountService.js index 134600ec..8abfe62f 100644 --- a/src/services/geminiAccountService.js +++ b/src/services/geminiAccountService.js @@ -1048,6 +1048,7 @@ async function getOauthClient(accessToken, refreshToken, proxyConfig = null) { // 验证凭据本地有效性 const { token } = await client.getAccessToken() + if (!token) { return false } @@ -1066,6 +1067,42 @@ async function loadCodeAssist(client, projectId = null, proxyConfig = null) { const CODE_ASSIST_API_VERSION = 'v1internal' 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.httpsAgent = proxyAgent + } + + await axios(tokenInfoConfig) + logger.info('📋 tokeninfo 接口验证成功') + + const userInfoConfig = { + url: 'https://www.googleapis.com/oauth2/v2/userinfo', + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + Accept: '*/*' + }, + timeout: 15000 + } + + if (proxyAgent) { + userInfoConfig.httpsAgent = proxyAgent + } + + await axios(userInfoConfig) + logger.info('📋 userinfo 接口获取成功') // 创建ClientMetadata const clientMetadata = { @@ -1100,7 +1137,6 @@ async function loadCodeAssist(client, projectId = null, proxyConfig = null) { } // 添加代理配置 - const proxyAgent = ProxyHelper.createProxyAgent(proxyConfig) if (proxyAgent) { axiosConfig.httpsAgent = proxyAgent logger.info(