cc / src /services /geminiAccountService.js
hequ's picture
Upload 224 files
6c6056a verified
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
}