const redisClient = require('../models/redis') const { v4: uuidv4 } = require('uuid') const crypto = require('crypto') const config = require('../../config/config') const logger = require('../utils/logger') const { OAuth2Client } = require('google-auth-library') const { maskToken } = require('../utils/tokenMask') const ProxyHelper = require('../utils/proxyHelper') const { logRefreshStart, logRefreshSuccess, logRefreshError, logTokenUsage, logRefreshSkipped } = require('../utils/tokenRefreshLogger') const tokenRefreshService = require('./tokenRefreshService') const LRUCache = require('../utils/lruCache') // 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'] // 加密相关常量 const ALGORITHM = 'aes-256-cbc' const ENCRYPTION_SALT = 'gemini-account-salt' const IV_LENGTH = 16 // 🚀 性能优化:缓存派生的加密密钥,避免每次重复计算 // scryptSync 是 CPU 密集型操作,缓存可以减少 95%+ 的 CPU 占用 let _encryptionKeyCache = null // 🔄 解密结果缓存,提高解密性能 const decryptCache = new LRUCache(500) // 生成加密密钥(使用与 claudeAccountService 相同的方法) function generateEncryptionKey() { if (!_encryptionKeyCache) { _encryptionKeyCache = crypto.scryptSync(config.security.encryptionKey, ENCRYPTION_SALT, 32) logger.info('🔑 Gemini encryption key derived and cached for performance optimization') } return _encryptionKeyCache } // Gemini 账户键前缀 const GEMINI_ACCOUNT_KEY_PREFIX = 'gemini_account:' const SHARED_GEMINI_ACCOUNTS_KEY = 'shared_gemini_accounts' const ACCOUNT_SESSION_MAPPING_PREFIX = 'gemini_session_account_mapping:' // 加密函数 function encrypt(text) { if (!text) { return '' } const key = generateEncryptionKey() const iv = crypto.randomBytes(IV_LENGTH) const cipher = crypto.createCipheriv(ALGORITHM, key, iv) let encrypted = cipher.update(text) encrypted = Buffer.concat([encrypted, cipher.final()]) return `${iv.toString('hex')}:${encrypted.toString('hex')}` } // 解密函数 function decrypt(text) { if (!text) { return '' } // 🎯 检查缓存 const cacheKey = crypto.createHash('sha256').update(text).digest('hex') const cached = decryptCache.get(cacheKey) if (cached !== undefined) { return cached } try { const key = generateEncryptionKey() // IV 是固定长度的 32 个十六进制字符(16 字节) const ivHex = text.substring(0, 32) const encryptedHex = text.substring(33) // 跳过冒号 const iv = Buffer.from(ivHex, 'hex') const encryptedText = Buffer.from(encryptedHex, 'hex') const decipher = crypto.createDecipheriv(ALGORITHM, key, iv) let decrypted = decipher.update(encryptedText) decrypted = Buffer.concat([decrypted, decipher.final()]) const result = decrypted.toString() // 💾 存入缓存(5分钟过期) decryptCache.set(cacheKey, result, 5 * 60 * 1000) // 📊 定期打印缓存统计 if ((decryptCache.hits + decryptCache.misses) % 1000 === 0) { decryptCache.printStats() } return result } catch (error) { logger.error('Decryption error:', error) return '' } } // 🧹 定期清理缓存(每10分钟) setInterval( () => { decryptCache.cleanup() logger.info('🧹 Gemini decrypt cache cleanup completed', decryptCache.getStats()) }, 10 * 60 * 1000 ) // 创建 OAuth2 客户端(支持代理配置) function createOAuth2Client(redirectUri = null, proxyConfig = null) { // 如果没有提供 redirectUri,使用默认值 const uri = redirectUri || 'http://localhost:45462' // 准备客户端选项 const clientOptions = { clientId: OAUTH_CLIENT_ID, clientSecret: OAUTH_CLIENT_SECRET, redirectUri: uri } // 如果有代理配置,设置 transporterOptions if (proxyConfig) { const proxyAgent = ProxyHelper.createProxyAgent(proxyConfig) if (proxyAgent) { // 通过 transporterOptions 传递代理配置给底层的 Gaxios clientOptions.transporterOptions = { agent: proxyAgent, httpsAgent: proxyAgent } logger.debug('Created OAuth2Client with proxy configuration') } } return new OAuth2Client(clientOptions) } // 生成授权 URL (支持 PKCE 和代理) async function generateAuthUrl(state = null, redirectUri = null, proxyConfig = null) { // 使用新的 redirect URI const finalRedirectUri = redirectUri || 'https://codeassist.google.com/authcode' const oAuth2Client = createOAuth2Client(finalRedirectUri, proxyConfig) if (proxyConfig) { logger.info( `🌐 Using proxy for Gemini auth URL generation: ${ProxyHelper.getProxyDescription(proxyConfig)}` ) } else { logger.debug('🌐 No proxy configured for Gemini auth URL generation') } // 生成 PKCE code verifier const codeVerifier = await oAuth2Client.generateCodeVerifierAsync() const stateValue = state || crypto.randomBytes(32).toString('hex') const authUrl = oAuth2Client.generateAuthUrl({ redirect_uri: finalRedirectUri, access_type: 'offline', scope: OAUTH_SCOPES, code_challenge_method: 'S256', code_challenge: codeVerifier.codeChallenge, state: stateValue, prompt: 'select_account' }) return { authUrl, state: stateValue, codeVerifier: codeVerifier.codeVerifier, redirectUri: finalRedirectUri } } // 轮询检查 OAuth 授权状态 async function pollAuthorizationStatus(sessionId, maxAttempts = 60, interval = 2000) { let attempts = 0 const client = redisClient.getClientSafe() while (attempts < maxAttempts) { try { const sessionData = await client.get(`oauth_session:${sessionId}`) if (!sessionData) { throw new Error('OAuth session not found') } const session = JSON.parse(sessionData) if (session.code) { // 授权码已获取,交换 tokens const tokens = await exchangeCodeForTokens(session.code) // 清理 session await client.del(`oauth_session:${sessionId}`) return { success: true, tokens } } if (session.error) { // 授权失败 await client.del(`oauth_session:${sessionId}`) return { success: false, error: session.error } } // 等待下一次轮询 await new Promise((resolve) => setTimeout(resolve, interval)) attempts++ } catch (error) { logger.error('Error polling authorization status:', error) throw error } } // 超时 await client.del(`oauth_session:${sessionId}`) return { success: false, error: 'Authorization timeout' } } // 交换授权码获取 tokens (支持 PKCE 和代理) async function exchangeCodeForTokens( code, redirectUri = null, codeVerifier = null, proxyConfig = null ) { try { // 创建带代理配置的 OAuth2Client const oAuth2Client = createOAuth2Client(redirectUri, proxyConfig) if (proxyConfig) { logger.info( `🌐 Using proxy for Gemini token exchange: ${ProxyHelper.getProxyDescription(proxyConfig)}` ) } else { logger.debug('🌐 No proxy configured for Gemini token exchange') } const tokenParams = { code, redirect_uri: redirectUri } // 如果提供了 codeVerifier,添加到参数中 if (codeVerifier) { tokenParams.codeVerifier = codeVerifier } const { tokens } = await oAuth2Client.getToken(tokenParams) // 转换为兼容格式 return { access_token: tokens.access_token, refresh_token: tokens.refresh_token, scope: tokens.scope || OAUTH_SCOPES.join(' '), token_type: tokens.token_type || 'Bearer', expiry_date: tokens.expiry_date || Date.now() + tokens.expires_in * 1000 } } catch (error) { logger.error('Error exchanging code for tokens:', error) throw new Error('Failed to exchange authorization code') } } // 刷新访问令牌 async function refreshAccessToken(refreshToken, proxyConfig = null) { // 创建带代理配置的 OAuth2Client const oAuth2Client = createOAuth2Client(null, proxyConfig) try { // 设置 refresh_token oAuth2Client.setCredentials({ refresh_token: refreshToken }) if (proxyConfig) { logger.info( `🔄 Using proxy for Gemini token refresh: ${ProxyHelper.maskProxyInfo(proxyConfig)}` ) } else { logger.debug('🔄 No proxy configured for Gemini token refresh') } // 调用 refreshAccessToken 获取新的 tokens const response = await oAuth2Client.refreshAccessToken() const { credentials } = response // 检查是否成功获取了新的 access_token if (!credentials || !credentials.access_token) { throw new Error('No access token returned from refresh') } logger.info( `🔄 Successfully refreshed Gemini token. New expiry: ${new Date(credentials.expiry_date).toISOString()}` ) return { access_token: credentials.access_token, refresh_token: credentials.refresh_token || refreshToken, // 保留原 refresh_token 如果没有返回新的 scope: credentials.scope || OAUTH_SCOPES.join(' '), token_type: credentials.token_type || 'Bearer', expiry_date: credentials.expiry_date || Date.now() + 3600000 // 默认1小时过期 } } catch (error) { logger.error('Error refreshing access token:', { message: error.message, code: error.code, response: error.response?.data, hasProxy: !!proxyConfig, proxy: proxyConfig ? ProxyHelper.maskProxyInfo(proxyConfig) : 'No proxy' }) throw new Error(`Failed to refresh access token: ${error.message}`) } } // 创建 Gemini 账户 async function createAccount(accountData) { const id = uuidv4() const now = new Date().toISOString() // 处理凭证数据 let geminiOauth = null let accessToken = '' let refreshToken = '' let expiresAt = '' if (accountData.geminiOauth || accountData.accessToken) { // 如果提供了完整的 OAuth 数据 if (accountData.geminiOauth) { geminiOauth = typeof accountData.geminiOauth === 'string' ? accountData.geminiOauth : JSON.stringify(accountData.geminiOauth) const oauthData = typeof accountData.geminiOauth === 'string' ? JSON.parse(accountData.geminiOauth) : accountData.geminiOauth accessToken = oauthData.access_token || '' refreshToken = oauthData.refresh_token || '' expiresAt = oauthData.expiry_date ? new Date(oauthData.expiry_date).toISOString() : '' } else { // 如果只提供了 access token ;({ accessToken } = accountData) refreshToken = accountData.refreshToken || '' // 构造完整的 OAuth 数据 geminiOauth = JSON.stringify({ access_token: accessToken, refresh_token: refreshToken, scope: accountData.scope || OAUTH_SCOPES.join(' '), token_type: accountData.tokenType || 'Bearer', expiry_date: accountData.expiryDate || Date.now() + 3600000 // 默认1小时 }) expiresAt = new Date(accountData.expiryDate || Date.now() + 3600000).toISOString() } } const account = { id, platform: 'gemini', // 标识为 Gemini 账户 name: accountData.name || 'Gemini Account', description: accountData.description || '', accountType: accountData.accountType || 'shared', isActive: 'true', status: 'active', // 调度相关 schedulable: accountData.schedulable !== undefined ? String(accountData.schedulable) : 'true', priority: accountData.priority || 50, // 调度优先级 (1-100,数字越小优先级越高) // OAuth 相关字段(加密存储) geminiOauth: geminiOauth ? encrypt(geminiOauth) : '', accessToken: accessToken ? encrypt(accessToken) : '', refreshToken: refreshToken ? encrypt(refreshToken) : '', expiresAt, // OAuth Token 过期时间(技术字段,自动刷新) // 只有OAuth方式才有scopes,手动添加的没有 scopes: accountData.geminiOauth ? accountData.scopes || OAUTH_SCOPES.join(' ') : '', // ✅ 新增:账户订阅到期时间(业务字段,手动管理) subscriptionExpiresAt: accountData.subscriptionExpiresAt || null, // 代理设置 proxy: accountData.proxy ? JSON.stringify(accountData.proxy) : '', // 项目 ID(Google Cloud/Workspace 账号需要) projectId: accountData.projectId || '', // 临时项目 ID(从 loadCodeAssist 接口自动获取) tempProjectId: accountData.tempProjectId || '', // 支持的模型列表(可选) supportedModels: accountData.supportedModels || [], // 空数组表示支持所有模型 // 时间戳 createdAt: now, updatedAt: now, lastUsedAt: '', lastRefreshAt: '' } // 保存到 Redis const client = redisClient.getClientSafe() await client.hset(`${GEMINI_ACCOUNT_KEY_PREFIX}${id}`, account) // 如果是共享账户,添加到共享账户集合 if (account.accountType === 'shared') { await client.sadd(SHARED_GEMINI_ACCOUNTS_KEY, id) } logger.info(`Created Gemini account: ${id}`) // 返回时解析代理配置 const returnAccount = { ...account } if (returnAccount.proxy) { try { returnAccount.proxy = JSON.parse(returnAccount.proxy) } catch (e) { returnAccount.proxy = null } } return returnAccount } // 获取账户 async function getAccount(accountId) { const client = redisClient.getClientSafe() const accountData = await client.hgetall(`${GEMINI_ACCOUNT_KEY_PREFIX}${accountId}`) if (!accountData || Object.keys(accountData).length === 0) { return null } // 解密敏感字段 if (accountData.geminiOauth) { accountData.geminiOauth = decrypt(accountData.geminiOauth) } if (accountData.accessToken) { accountData.accessToken = decrypt(accountData.accessToken) } if (accountData.refreshToken) { accountData.refreshToken = decrypt(accountData.refreshToken) } // 解析代理配置 if (accountData.proxy) { try { accountData.proxy = JSON.parse(accountData.proxy) } catch (e) { // 如果解析失败,保持原样或设置为null accountData.proxy = null } } // 转换 schedulable 字符串为布尔值(与 claudeConsoleAccountService 保持一致) accountData.schedulable = accountData.schedulable !== 'false' // 默认为true,只有明确设置为'false'才为false return accountData } // 更新账户 async function updateAccount(accountId, updates) { const existingAccount = await getAccount(accountId) if (!existingAccount) { throw new Error('Account not found') } const now = new Date().toISOString() updates.updatedAt = now // 检查是否新增了 refresh token // existingAccount.refreshToken 已经是解密后的值了(从 getAccount 返回) const oldRefreshToken = existingAccount.refreshToken || '' let needUpdateExpiry = false // 处理代理设置 if (updates.proxy !== undefined) { updates.proxy = updates.proxy ? JSON.stringify(updates.proxy) : '' } // 处理 schedulable 字段,确保正确转换为字符串存储 if (updates.schedulable !== undefined) { updates.schedulable = updates.schedulable.toString() } // 加密敏感字段 if (updates.geminiOauth) { updates.geminiOauth = encrypt( typeof updates.geminiOauth === 'string' ? updates.geminiOauth : JSON.stringify(updates.geminiOauth) ) } if (updates.accessToken) { updates.accessToken = encrypt(updates.accessToken) } if (updates.refreshToken) { updates.refreshToken = encrypt(updates.refreshToken) // 如果之前没有 refresh token,现在有了,标记需要更新过期时间 if (!oldRefreshToken && updates.refreshToken) { needUpdateExpiry = true } } // 更新账户类型时处理共享账户集合 const client = redisClient.getClientSafe() if (updates.accountType && updates.accountType !== existingAccount.accountType) { if (updates.accountType === 'shared') { await client.sadd(SHARED_GEMINI_ACCOUNTS_KEY, accountId) } else { await client.srem(SHARED_GEMINI_ACCOUNTS_KEY, accountId) } } // ✅ 关键:如果新增了 refresh token,只更新 token 过期时间 // 不要覆盖 subscriptionExpiresAt if (needUpdateExpiry) { const newExpiry = new Date(Date.now() + 10 * 60 * 1000).toISOString() updates.expiresAt = newExpiry // 只更新 OAuth Token 过期时间 // ⚠️ 重要:不要修改 subscriptionExpiresAt logger.info( `🔄 New refresh token added for Gemini account ${accountId}, setting token expiry to 10 minutes` ) } // ✅ 如果通过路由映射更新了 subscriptionExpiresAt,直接保存 // subscriptionExpiresAt 是业务字段,与 token 刷新独立 if (updates.subscriptionExpiresAt !== undefined) { // 直接保存,不做任何调整 } // 如果通过 geminiOauth 更新,也要检查是否新增了 refresh token if (updates.geminiOauth && !oldRefreshToken) { const oauthData = typeof updates.geminiOauth === 'string' ? JSON.parse(decrypt(updates.geminiOauth)) : updates.geminiOauth if (oauthData.refresh_token) { // 如果 expiry_date 设置的时间过长(超过1小时),调整为10分钟 const providedExpiry = oauthData.expiry_date || 0 const currentTime = Date.now() const oneHour = 60 * 60 * 1000 if (providedExpiry - currentTime > oneHour) { const newExpiry = new Date(currentTime + 10 * 60 * 1000).toISOString() updates.expiresAt = newExpiry logger.info( `🔄 Adjusted expiry time to 10 minutes for Gemini account ${accountId} with refresh token` ) } } } // 检查是否手动禁用了账号,如果是则发送webhook通知 if (updates.isActive === 'false' && existingAccount.isActive !== 'false') { try { const webhookNotifier = require('../utils/webhookNotifier') await webhookNotifier.sendAccountAnomalyNotification({ accountId, accountName: updates.name || existingAccount.name || 'Unknown Account', platform: 'gemini', status: 'disabled', errorCode: 'GEMINI_MANUALLY_DISABLED', reason: 'Account manually disabled by administrator' }) } catch (webhookError) { logger.error('Failed to send webhook notification for manual account disable:', webhookError) } } await client.hset(`${GEMINI_ACCOUNT_KEY_PREFIX}${accountId}`, updates) logger.info(`Updated Gemini account: ${accountId}`) // 合并更新后的账户数据 const updatedAccount = { ...existingAccount, ...updates } // 返回时解析代理配置 if (updatedAccount.proxy && typeof updatedAccount.proxy === 'string') { try { updatedAccount.proxy = JSON.parse(updatedAccount.proxy) } catch (e) { updatedAccount.proxy = null } } return updatedAccount } // 删除账户 async function deleteAccount(accountId) { const account = await getAccount(accountId) if (!account) { throw new Error('Account not found') } // 从 Redis 删除 const client = redisClient.getClientSafe() await client.del(`${GEMINI_ACCOUNT_KEY_PREFIX}${accountId}`) // 从共享账户集合中移除 if (account.accountType === 'shared') { await client.srem(SHARED_GEMINI_ACCOUNTS_KEY, accountId) } // 清理会话映射 const sessionMappings = await client.keys(`${ACCOUNT_SESSION_MAPPING_PREFIX}*`) for (const key of sessionMappings) { const mappedAccountId = await client.get(key) if (mappedAccountId === accountId) { await client.del(key) } } logger.info(`Deleted Gemini account: ${accountId}`) return true } // 获取所有账户 async function getAllAccounts() { const client = redisClient.getClientSafe() const keys = await client.keys(`${GEMINI_ACCOUNT_KEY_PREFIX}*`) const accounts = [] for (const key of keys) { const accountData = await client.hgetall(key) if (accountData && Object.keys(accountData).length > 0) { // 获取限流状态信息 const rateLimitInfo = await getAccountRateLimitInfo(accountData.id) // 解析代理配置 if (accountData.proxy) { try { accountData.proxy = JSON.parse(accountData.proxy) } catch (e) { // 如果解析失败,设置为null accountData.proxy = null } } // 转换 schedulable 字符串为布尔值(与 getAccount 保持一致) accountData.schedulable = accountData.schedulable !== 'false' // 默认为true,只有明确设置为'false'才为false const tokenExpiresAt = accountData.expiresAt || null const subscriptionExpiresAt = accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== '' ? accountData.subscriptionExpiresAt : null // 不解密敏感字段,只返回基本信息 accounts.push({ ...accountData, geminiOauth: accountData.geminiOauth ? '[ENCRYPTED]' : '', accessToken: accountData.accessToken ? '[ENCRYPTED]' : '', refreshToken: accountData.refreshToken ? '[ENCRYPTED]' : '', // ✅ 前端显示订阅过期时间(业务字段) // 注意:前端看到的 expiresAt 实际上是 subscriptionExpiresAt tokenExpiresAt, subscriptionExpiresAt, expiresAt: subscriptionExpiresAt, // 添加 scopes 字段用于判断认证方式 // 处理空字符串和默认值的情况 scopes: accountData.scopes && accountData.scopes.trim() ? accountData.scopes.split(' ') : [], // 添加 hasRefreshToken 标记 hasRefreshToken: !!accountData.refreshToken, // 添加限流状态信息(统一格式) rateLimitStatus: rateLimitInfo ? { isRateLimited: rateLimitInfo.isRateLimited, rateLimitedAt: rateLimitInfo.rateLimitedAt, minutesRemaining: rateLimitInfo.minutesRemaining } : { isRateLimited: false, rateLimitedAt: null, minutesRemaining: 0 } }) } } return accounts } // 选择可用账户(支持专属和共享账户) async function selectAvailableAccount(apiKeyId, sessionHash = null) { // 首先检查是否有粘性会话 const client = redisClient.getClientSafe() if (sessionHash) { const mappedAccountId = await client.get(`${ACCOUNT_SESSION_MAPPING_PREFIX}${sessionHash}`) if (mappedAccountId) { const account = await getAccount(mappedAccountId) if (account && account.isActive === 'true' && !isTokenExpired(account)) { logger.debug(`Using sticky session account: ${mappedAccountId}`) return account } } } // 获取 API Key 信息 const apiKeyData = await client.hgetall(`api_key:${apiKeyId}`) // 检查是否绑定了 Gemini 账户 if (apiKeyData.geminiAccountId) { const account = await getAccount(apiKeyData.geminiAccountId) if (account && account.isActive === 'true') { // 检查 token 是否过期 const isExpired = isTokenExpired(account) // 记录token使用情况 logTokenUsage(account.id, account.name, 'gemini', account.expiresAt, isExpired) if (isExpired) { await refreshAccountToken(account.id) return await getAccount(account.id) } // 创建粘性会话映射 if (sessionHash) { await client.setex( `${ACCOUNT_SESSION_MAPPING_PREFIX}${sessionHash}`, 3600, // 1小时过期 account.id ) } return account } } // 从共享账户池选择 const sharedAccountIds = await client.smembers(SHARED_GEMINI_ACCOUNTS_KEY) const availableAccounts = [] for (const accountId of sharedAccountIds) { const account = await getAccount(accountId) if ( account && account.isActive === 'true' && !isRateLimited(account) && !isSubscriptionExpired(account) ) { availableAccounts.push(account) } else if (account && isSubscriptionExpired(account)) { logger.debug( `⏰ Skipping expired Gemini account: ${account.name}, expired at ${account.subscriptionExpiresAt}` ) } } if (availableAccounts.length === 0) { throw new Error('No available Gemini accounts') } // 选择最少使用的账户 availableAccounts.sort((a, b) => { const aLastUsed = a.lastUsedAt ? new Date(a.lastUsedAt).getTime() : 0 const bLastUsed = b.lastUsedAt ? new Date(b.lastUsedAt).getTime() : 0 return aLastUsed - bLastUsed }) const selectedAccount = availableAccounts[0] // 检查并刷新 token const isExpired = isTokenExpired(selectedAccount) // 记录token使用情况 logTokenUsage( selectedAccount.id, selectedAccount.name, 'gemini', selectedAccount.expiresAt, isExpired ) if (isExpired) { await refreshAccountToken(selectedAccount.id) return await getAccount(selectedAccount.id) } // 创建粘性会话映射 if (sessionHash) { await client.setex(`${ACCOUNT_SESSION_MAPPING_PREFIX}${sessionHash}`, 3600, selectedAccount.id) } return selectedAccount } // 检查 token 是否过期 function isTokenExpired(account) { if (!account.expiresAt) { return true } const expiryTime = new Date(account.expiresAt).getTime() const now = Date.now() const buffer = 10 * 1000 // 10秒缓冲 return now >= expiryTime - buffer } /** * 检查账户订阅是否过期 * @param {Object} account - 账户对象 * @returns {boolean} - true: 已过期, false: 未过期 */ function isSubscriptionExpired(account) { if (!account.subscriptionExpiresAt) { return false // 未设置视为永不过期 } const expiryDate = new Date(account.subscriptionExpiresAt) return expiryDate <= new Date() } // 检查账户是否被限流 function isRateLimited(account) { if (account.rateLimitStatus === 'limited' && account.rateLimitedAt) { const limitedAt = new Date(account.rateLimitedAt).getTime() const now = Date.now() const limitDuration = 60 * 60 * 1000 // 1小时 return now < limitedAt + limitDuration } return false } // 刷新账户 token async function refreshAccountToken(accountId) { let lockAcquired = false let account = null try { account = await getAccount(accountId) if (!account) { throw new Error('Account not found') } if (!account.refreshToken) { throw new Error('No refresh token available') } // 尝试获取分布式锁 lockAcquired = await tokenRefreshService.acquireRefreshLock(accountId, 'gemini') if (!lockAcquired) { // 如果无法获取锁,说明另一个进程正在刷新 logger.info( `🔒 Token refresh already in progress for Gemini account: ${account.name} (${accountId})` ) logRefreshSkipped(accountId, account.name, 'gemini', 'already_locked') // 等待一段时间后返回,期望其他进程已完成刷新 await new Promise((resolve) => setTimeout(resolve, 2000)) // 重新获取账户数据(可能已被其他进程刷新) const updatedAccount = await getAccount(accountId) if (updatedAccount && updatedAccount.accessToken) { const accessToken = decrypt(updatedAccount.accessToken) return { access_token: accessToken, refresh_token: updatedAccount.refreshToken ? decrypt(updatedAccount.refreshToken) : '', expiry_date: updatedAccount.expiresAt ? new Date(updatedAccount.expiresAt).getTime() : 0, scope: updatedAccount.scope || OAUTH_SCOPES.join(' '), token_type: 'Bearer' } } throw new Error('Token refresh in progress by another process') } // 记录开始刷新 logRefreshStart(accountId, account.name, 'gemini', 'manual_refresh') logger.info(`🔄 Starting token refresh for Gemini account: ${account.name} (${accountId})`) // account.refreshToken 已经是解密后的值(从 getAccount 返回) // 传入账户的代理配置 const newTokens = await refreshAccessToken(account.refreshToken, account.proxy) // 更新账户信息 const updates = { accessToken: newTokens.access_token, refreshToken: newTokens.refresh_token || account.refreshToken, expiresAt: new Date(newTokens.expiry_date).toISOString(), lastRefreshAt: new Date().toISOString(), geminiOauth: JSON.stringify(newTokens), status: 'active', // 刷新成功后,将状态更新为 active errorMessage: '' // 清空错误信息 } await updateAccount(accountId, updates) // 记录刷新成功 logRefreshSuccess(accountId, account.name, 'gemini', { accessToken: newTokens.access_token, refreshToken: newTokens.refresh_token, expiresAt: newTokens.expiry_date, scopes: newTokens.scope }) logger.info( `Refreshed token for Gemini account: ${accountId} - Access Token: ${maskToken(newTokens.access_token)}` ) return newTokens } catch (error) { // 记录刷新失败 logRefreshError(accountId, account ? account.name : 'Unknown', 'gemini', error) logger.error(`Failed to refresh token for account ${accountId}:`, error) // 标记账户为错误状态(只有在账户存在时) if (account) { try { await updateAccount(accountId, { status: 'error', errorMessage: error.message }) // 发送Webhook通知 try { const webhookNotifier = require('../utils/webhookNotifier') await webhookNotifier.sendAccountAnomalyNotification({ accountId, accountName: account.name, platform: 'gemini', status: 'error', errorCode: 'GEMINI_ERROR', reason: `Token refresh failed: ${error.message}` }) } catch (webhookError) { logger.error('Failed to send webhook notification:', webhookError) } } catch (updateError) { logger.error('Failed to update account status after refresh error:', updateError) } } throw error } finally { // 释放锁 if (lockAcquired) { await tokenRefreshService.releaseRefreshLock(accountId, 'gemini') } } } // 标记账户被使用 async function markAccountUsed(accountId) { await updateAccount(accountId, { lastUsedAt: new Date().toISOString() }) } // 设置账户限流状态 async function setAccountRateLimited(accountId, isLimited = true) { const updates = isLimited ? { rateLimitStatus: 'limited', rateLimitedAt: new Date().toISOString() } : { rateLimitStatus: '', rateLimitedAt: '' } await updateAccount(accountId, updates) } // 获取账户的限流信息(参考 claudeAccountService 的实现) async function getAccountRateLimitInfo(accountId) { try { const account = await getAccount(accountId) if (!account) { return null } if (account.rateLimitStatus === 'limited' && account.rateLimitedAt) { const rateLimitedAt = new Date(account.rateLimitedAt) const now = new Date() const minutesSinceRateLimit = Math.floor((now - rateLimitedAt) / (1000 * 60)) // Gemini 限流持续时间为 1 小时 const minutesRemaining = Math.max(0, 60 - minutesSinceRateLimit) const rateLimitEndAt = new Date(rateLimitedAt.getTime() + 60 * 60 * 1000).toISOString() return { isRateLimited: minutesRemaining > 0, rateLimitedAt: account.rateLimitedAt, minutesSinceRateLimit, minutesRemaining, rateLimitEndAt } } return { isRateLimited: false, rateLimitedAt: null, minutesSinceRateLimit: 0, minutesRemaining: 0, rateLimitEndAt: null } } catch (error) { logger.error(`❌ Failed to get rate limit info for Gemini account: ${accountId}`, error) return null } } // 获取配置的OAuth客户端 - 参考GeminiCliSimulator的getOauthClient方法(支持代理) async function getOauthClient(accessToken, refreshToken, proxyConfig = null) { const client = createOAuth2Client(null, proxyConfig) const creds = { access_token: accessToken, refresh_token: refreshToken, scope: 'https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/userinfo.profile openid https://www.googleapis.com/auth/userinfo.email', token_type: 'Bearer', expiry_date: 1754269905646 } if (proxyConfig) { logger.info( `🌐 Using proxy for Gemini OAuth client: ${ProxyHelper.getProxyDescription(proxyConfig)}` ) } else { logger.debug('🌐 No proxy configured for Gemini OAuth client') } // 设置凭据 client.setCredentials(creds) // 验证凭据本地有效性 const { token } = await client.getAccessToken() if (!token) { return false } // 验证服务器端token状态(检查是否被撤销) await client.getTokenInfo(token) logger.info('✅ OAuth客户端已创建') return client } // 调用 Google Code Assist API 的 loadCodeAssist 方法(支持代理) async function loadCodeAssist(client, projectId = null, 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) 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 const clientMetadata = { ideType: 'IDE_UNSPECIFIED', platform: 'PLATFORM_UNSPECIFIED', pluginType: 'GEMINI' } // 只有当projectId存在时才添加duetProject if (projectId) { clientMetadata.duetProject = projectId } const request = { metadata: clientMetadata } // 只有当projectId存在时才添加cloudaicompanionProject if (projectId) { request.cloudaicompanionProject = projectId } const axiosConfig = { url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:loadCodeAssist`, method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, data: request, timeout: 30000 } // 添加代理配置 if (proxyAgent) { axiosConfig.httpAgent = proxyAgent axiosConfig.httpsAgent = proxyAgent axiosConfig.proxy = false logger.info( `🌐 Using proxy for Gemini loadCodeAssist: ${ProxyHelper.getProxyDescription(proxyConfig)}` ) } else { logger.debug('🌐 No proxy configured for Gemini loadCodeAssist') } const response = await axios(axiosConfig) logger.info('📋 loadCodeAssist API调用成功') return response.data } // 获取onboard层级 - 参考GeminiCliSimulator的getOnboardTier方法 function getOnboardTier(loadRes) { // 用户层级枚举 const UserTierId = { LEGACY: 'LEGACY', FREE: 'FREE', PRO: 'PRO' } if (loadRes.currentTier) { return loadRes.currentTier } for (const tier of loadRes.allowedTiers || []) { if (tier.isDefault) { return tier } } return { name: '', description: '', id: UserTierId.LEGACY, userDefinedCloudaicompanionProject: true } } // 调用 Google Code Assist API 的 onboardUser 方法(包含轮询逻辑,支持代理) async function onboardUser(client, tierId, projectId, clientMetadata, 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 onboardReq = { tierId, metadata: clientMetadata } // 只有当projectId存在时才添加cloudaicompanionProject if (projectId) { onboardReq.cloudaicompanionProject = projectId } // 创建基础axios配置 const baseAxiosConfig = { url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:onboardUser`, method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, data: onboardReq, timeout: 30000 } // 添加代理配置 const proxyAgent = ProxyHelper.createProxyAgent(proxyConfig) if (proxyAgent) { baseAxiosConfig.httpAgent = proxyAgent baseAxiosConfig.httpsAgent = proxyAgent baseAxiosConfig.proxy = false logger.info( `🌐 Using proxy for Gemini onboardUser: ${ProxyHelper.getProxyDescription(proxyConfig)}` ) } else { logger.debug('🌐 No proxy configured for Gemini onboardUser') } logger.info('📋 开始onboardUser API调用', { tierId, projectId, hasProjectId: !!projectId, isFreeTier: tierId === 'free-tier' || tierId === 'FREE' }) // 轮询onboardUser直到长运行操作完成 let lroRes = await axios(baseAxiosConfig) let attempts = 0 const maxAttempts = 12 // 最多等待1分钟(5秒 * 12次) while (!lroRes.data.done && attempts < maxAttempts) { logger.info(`⏳ 等待onboardUser完成... (${attempts + 1}/${maxAttempts})`) await new Promise((resolve) => setTimeout(resolve, 5000)) lroRes = await axios(baseAxiosConfig) attempts++ } if (!lroRes.data.done) { throw new Error('onboardUser操作超时') } logger.info('✅ onboardUser API调用完成') return lroRes.data } // 完整的用户设置流程 - 参考setup.ts的逻辑(支持代理) async function setupUser( client, initialProjectId = null, clientMetadata = null, proxyConfig = null ) { logger.info('🚀 setupUser 开始', { initialProjectId, hasClientMetadata: !!clientMetadata }) let projectId = initialProjectId || process.env.GOOGLE_CLOUD_PROJECT || null logger.info('📋 初始项目ID', { projectId, fromEnv: !!process.env.GOOGLE_CLOUD_PROJECT }) // 默认的ClientMetadata if (!clientMetadata) { clientMetadata = { ideType: 'IDE_UNSPECIFIED', platform: 'PLATFORM_UNSPECIFIED', pluginType: 'GEMINI', duetProject: projectId } logger.info('🔧 使用默认 ClientMetadata') } // 调用loadCodeAssist logger.info('📞 调用 loadCodeAssist...') const loadRes = await loadCodeAssist(client, projectId, proxyConfig) logger.info('✅ loadCodeAssist 完成', { hasCloudaicompanionProject: !!loadRes.cloudaicompanionProject }) // 如果没有projectId,尝试从loadRes获取 if (!projectId && loadRes.cloudaicompanionProject) { projectId = loadRes.cloudaicompanionProject logger.info('📋 从 loadCodeAssist 获取项目ID', { projectId }) } const tier = getOnboardTier(loadRes) logger.info('🎯 获取用户层级', { tierId: tier.id, userDefinedProject: tier.userDefinedCloudaicompanionProject }) if (tier.userDefinedCloudaiCompanionProject && !projectId) { throw new Error('此账号需要设置GOOGLE_CLOUD_PROJECT环境变量或提供projectId') } // 调用onboardUser logger.info('📞 调用 onboardUser...', { tierId: tier.id, projectId }) const lroRes = await onboardUser(client, tier.id, projectId, clientMetadata, proxyConfig) logger.info('✅ onboardUser 完成', { hasDone: !!lroRes.done, hasResponse: !!lroRes.response }) const result = { projectId: lroRes.response?.cloudaicompanionProject?.id || projectId || '', userTier: tier.id, loadRes, onboardRes: lroRes.response || {} } logger.info('🎯 setupUser 完成', { resultProjectId: result.projectId, userTier: result.userTier }) return result } // 调用 Code Assist API 计算 token 数量(支持代理) async function countTokens(client, contents, model = 'gemini-2.0-flash-exp', 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() // 按照 gemini-cli 的转换格式构造请求 const request = { request: { model: `models/${model}`, contents } } logger.info('📊 countTokens API调用开始', { model, contentsLength: contents.length }) const axiosConfig = { url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:countTokens`, method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, data: request, timeout: 30000 } // 添加代理配置 const proxyAgent = ProxyHelper.createProxyAgent(proxyConfig) if (proxyAgent) { axiosConfig.httpAgent = proxyAgent axiosConfig.httpsAgent = proxyAgent axiosConfig.proxy = false logger.info( `🌐 Using proxy for Gemini countTokens: ${ProxyHelper.getProxyDescription(proxyConfig)}` ) } else { logger.debug('🌐 No proxy configured for Gemini countTokens') } const response = await axios(axiosConfig) logger.info('✅ countTokens API调用成功', { totalTokens: response.data.totalTokens }) return response.data } // 调用 Code Assist API 生成内容(非流式) async function generateContent( client, requestData, userPromptId, projectId = null, sessionId = null, 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() // 按照 gemini-cli 的转换格式构造请求 const request = { model: requestData.model, request: { ...requestData.request, session_id: sessionId } } // 只有当 userPromptId 存在时才添加 if (userPromptId) { request.user_prompt_id = userPromptId } // 只有当projectId存在时才添加project字段 if (projectId) { request.project = projectId } logger.info('🤖 generateContent API调用开始', { model: requestData.model, userPromptId, projectId, sessionId }) // 添加详细的请求日志 logger.info('📦 generateContent 请求详情', { url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:generateContent`, requestBody: JSON.stringify(request, null, 2) }) const axiosConfig = { url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:generateContent`, method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, data: request, timeout: 60000 // 生成内容可能需要更长时间 } // 添加代理配置 const proxyAgent = ProxyHelper.createProxyAgent(proxyConfig) if (proxyAgent) { axiosConfig.httpAgent = proxyAgent axiosConfig.httpsAgent = proxyAgent axiosConfig.proxy = false logger.info( `🌐 Using proxy for Gemini generateContent: ${ProxyHelper.getProxyDescription(proxyConfig)}` ) } else { logger.debug('🌐 No proxy configured for Gemini generateContent') } const response = await axios(axiosConfig) logger.info('✅ generateContent API调用成功') return response.data } // 调用 Code Assist API 生成内容(流式) async function generateContentStream( client, requestData, userPromptId, projectId = null, sessionId = null, signal = null, 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() // 按照 gemini-cli 的转换格式构造请求 const request = { model: requestData.model, request: { ...requestData.request, session_id: sessionId } } // 只有当 userPromptId 存在时才添加 if (userPromptId) { request.user_prompt_id = userPromptId } // 只有当projectId存在时才添加project字段 if (projectId) { request.project = projectId } logger.info('🌊 streamGenerateContent API调用开始', { model: requestData.model, userPromptId, projectId, sessionId }) const axiosConfig = { url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:streamGenerateContent`, method: 'POST', params: { alt: 'sse' }, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, data: request, responseType: 'stream', timeout: 60000 } // 添加代理配置 const proxyAgent = ProxyHelper.createProxyAgent(proxyConfig) if (proxyAgent) { axiosConfig.httpAgent = proxyAgent axiosConfig.httpsAgent = proxyAgent axiosConfig.proxy = false logger.info( `🌐 Using proxy for Gemini streamGenerateContent: ${ProxyHelper.getProxyDescription(proxyConfig)}` ) } else { logger.debug('🌐 No proxy configured for Gemini streamGenerateContent') } // 如果提供了中止信号,添加到配置中 if (signal) { axiosConfig.signal = signal } const response = await axios(axiosConfig) logger.info('✅ streamGenerateContent API调用成功,开始流式传输') return response.data // 返回流对象 } // 更新账户的临时项目 ID async function updateTempProjectId(accountId, tempProjectId) { if (!tempProjectId) { return } try { const account = await getAccount(accountId) if (!account) { logger.warn(`Account ${accountId} not found when updating tempProjectId`) return } // 只有在没有固定项目 ID 的情况下才更新临时项目 ID if (!account.projectId && tempProjectId !== account.tempProjectId) { await updateAccount(accountId, { tempProjectId }) logger.info(`Updated tempProjectId for account ${accountId}: ${tempProjectId}`) } } catch (error) { logger.error(`Failed to update tempProjectId for account ${accountId}:`, error) } } module.exports = { generateAuthUrl, pollAuthorizationStatus, exchangeCodeForTokens, refreshAccessToken, createAccount, getAccount, updateAccount, deleteAccount, getAllAccounts, selectAvailableAccount, refreshAccountToken, markAccountUsed, setAccountRateLimited, getAccountRateLimitInfo, isTokenExpired, getOauthClient, loadCodeAssist, getOnboardTier, onboardUser, setupUser, encrypt, decrypt, generateEncryptionKey, decryptCache, // 暴露缓存对象以便测试和监控 countTokens, generateContent, generateContentStream, updateTempProjectId, OAUTH_CLIENT_ID, OAUTH_SCOPES }