Files
claude-relay-service/src/routes/admin/geminiAccounts.js

495 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const express = require('express')
const geminiAccountService = require('../../services/geminiAccountService')
const accountGroupService = require('../../services/accountGroupService')
const apiKeyService = require('../../services/apiKeyService')
const redis = require('../../models/redis')
const { authenticateAdmin } = require('../../middleware/auth')
const logger = require('../../utils/logger')
const webhookNotifier = require('../../utils/webhookNotifier')
const { formatAccountExpiry, mapExpiryField } = require('./utils')
const router = express.Router()
// 🤖 Gemini OAuth 账户管理
// 生成 Gemini OAuth 授权 URL
router.post('/generate-auth-url', authenticateAdmin, async (req, res) => {
try {
const { state, proxy } = req.body // 接收代理配置
// 使用新的 codeassist.google.com 回调地址
const redirectUri = 'https://codeassist.google.com/authcode'
logger.info(`Generating Gemini OAuth URL with redirect_uri: ${redirectUri}`)
const {
authUrl,
state: authState,
codeVerifier,
redirectUri: finalRedirectUri
} = await geminiAccountService.generateAuthUrl(state, redirectUri, proxy)
// 创建 OAuth 会话,包含 codeVerifier 和代理配置
const sessionId = authState
await redis.setOAuthSession(sessionId, {
state: authState,
type: 'gemini',
redirectUri: finalRedirectUri,
codeVerifier, // 保存 PKCE code verifier
proxy: proxy || null, // 保存代理配置
createdAt: new Date().toISOString()
})
logger.info(`Generated Gemini OAuth URL with session: ${sessionId}`)
return res.json({
success: true,
data: {
authUrl,
sessionId
}
})
} catch (error) {
logger.error('❌ Failed to generate Gemini auth URL:', error)
return res.status(500).json({ error: 'Failed to generate auth URL', message: error.message })
}
})
// 轮询 Gemini OAuth 授权状态
router.post('/poll-auth-status', authenticateAdmin, async (req, res) => {
try {
const { sessionId } = req.body
if (!sessionId) {
return res.status(400).json({ error: 'Session ID is required' })
}
const result = await geminiAccountService.pollAuthorizationStatus(sessionId)
if (result.success) {
logger.success(`✅ Gemini OAuth authorization successful for session: ${sessionId}`)
return res.json({ success: true, data: { tokens: result.tokens } })
} else {
return res.json({ success: false, error: result.error })
}
} catch (error) {
logger.error('❌ Failed to poll Gemini auth status:', error)
return res.status(500).json({ error: 'Failed to poll auth status', message: error.message })
}
})
// 交换 Gemini 授权码
router.post('/exchange-code', authenticateAdmin, async (req, res) => {
try {
const { code, sessionId, proxy: requestProxy } = req.body
if (!code) {
return res.status(400).json({ error: 'Authorization code is required' })
}
let redirectUri = 'https://codeassist.google.com/authcode'
let codeVerifier = null
let proxyConfig = null
// 如果提供了 sessionId从 OAuth 会话中获取信息
if (sessionId) {
const sessionData = await redis.getOAuthSession(sessionId)
if (sessionData) {
const {
redirectUri: sessionRedirectUri,
codeVerifier: sessionCodeVerifier,
proxy
} = sessionData
redirectUri = sessionRedirectUri || redirectUri
codeVerifier = sessionCodeVerifier
proxyConfig = proxy // 获取代理配置
logger.info(
`Using session redirect_uri: ${redirectUri}, has codeVerifier: ${!!codeVerifier}, has proxy from session: ${!!proxyConfig}`
)
}
}
// 如果请求体中直接提供了代理配置,优先使用它
if (requestProxy) {
proxyConfig = requestProxy
logger.info(
`Using proxy from request body: ${proxyConfig ? JSON.stringify(proxyConfig) : 'none'}`
)
}
const tokens = await geminiAccountService.exchangeCodeForTokens(
code,
redirectUri,
codeVerifier,
proxyConfig // 传递代理配置
)
// 清理 OAuth 会话
if (sessionId) {
await redis.deleteOAuthSession(sessionId)
}
logger.success('✅ Successfully exchanged Gemini authorization code')
return res.json({ success: true, data: { tokens } })
} catch (error) {
logger.error('❌ Failed to exchange Gemini authorization code:', error)
return res.status(500).json({ error: 'Failed to exchange code', message: error.message })
}
})
// 获取所有 Gemini 账户
router.get('/', authenticateAdmin, async (req, res) => {
try {
const { platform, groupId } = req.query
let accounts = await geminiAccountService.getAllAccounts()
// 根据查询参数进行筛选
if (platform && platform !== 'all' && platform !== 'gemini') {
// 如果指定了其他平台,返回空数组
accounts = []
}
// 如果指定了分组筛选
if (groupId && groupId !== 'all') {
if (groupId === 'ungrouped') {
// 筛选未分组账户
const filteredAccounts = []
for (const account of accounts) {
const groups = await accountGroupService.getAccountGroups(account.id)
if (!groups || groups.length === 0) {
filteredAccounts.push(account)
}
}
accounts = filteredAccounts
} else {
// 筛选特定分组的账户
const groupMembers = await accountGroupService.getGroupMembers(groupId)
accounts = accounts.filter((account) => groupMembers.includes(account.id))
}
}
// 为每个账户添加使用统计信息与Claude账户相同的逻辑
const accountsWithStats = await Promise.all(
accounts.map(async (account) => {
try {
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
const groupInfos = await accountGroupService.getAccountGroups(account.id)
const formattedAccount = formatAccountExpiry(account)
return {
...formattedAccount,
groupInfos,
usage: {
daily: usageStats.daily,
total: usageStats.total,
averages: usageStats.averages
}
}
} catch (statsError) {
logger.warn(
`⚠️ Failed to get usage stats for Gemini account ${account.id}:`,
statsError.message
)
// 如果获取统计失败,返回空统计
try {
const groupInfos = await accountGroupService.getAccountGroups(account.id)
const formattedAccount = formatAccountExpiry(account)
return {
...formattedAccount,
groupInfos,
usage: {
daily: { tokens: 0, requests: 0, allTokens: 0 },
total: { tokens: 0, requests: 0, allTokens: 0 },
averages: { rpm: 0, tpm: 0 }
}
}
} catch (groupError) {
logger.warn(
`⚠️ Failed to get group info for account ${account.id}:`,
groupError.message
)
return {
...account,
groupInfos: [],
usage: {
daily: { tokens: 0, requests: 0, allTokens: 0 },
total: { tokens: 0, requests: 0, allTokens: 0 },
averages: { rpm: 0, tpm: 0 }
}
}
}
}
})
)
return res.json({ success: true, data: accountsWithStats })
} catch (error) {
logger.error('❌ Failed to get Gemini accounts:', error)
return res.status(500).json({ error: 'Failed to get accounts', message: error.message })
}
})
// 创建新的 Gemini 账户
router.post('/', authenticateAdmin, async (req, res) => {
try {
const accountData = req.body
// 输入验证
if (!accountData.name) {
return res.status(400).json({ error: 'Account name is required' })
}
// 验证accountType的有效性
if (
accountData.accountType &&
!['shared', 'dedicated', 'group'].includes(accountData.accountType)
) {
return res
.status(400)
.json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
}
// 如果是分组类型验证groupId或groupIds
if (
accountData.accountType === 'group' &&
!accountData.groupId &&
(!accountData.groupIds || accountData.groupIds.length === 0)
) {
return res.status(400).json({ error: 'Group ID is required for group type accounts' })
}
const newAccount = await geminiAccountService.createAccount(accountData)
// 如果是分组类型,处理分组绑定
if (accountData.accountType === 'group') {
if (accountData.groupIds && accountData.groupIds.length > 0) {
// 多分组模式
await accountGroupService.setAccountGroups(newAccount.id, accountData.groupIds, 'gemini')
logger.info(
`🏢 Added Gemini account ${newAccount.id} to groups: ${accountData.groupIds.join(', ')}`
)
} else if (accountData.groupId) {
// 单分组模式(向后兼容)
await accountGroupService.addAccountToGroup(newAccount.id, accountData.groupId, 'gemini')
}
}
logger.success(`🏢 Admin created new Gemini account: ${accountData.name}`)
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 })
}
})
// 更新 Gemini 账户
router.put('/:accountId', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const updates = req.body
// 验证accountType的有效性
if (updates.accountType && !['shared', 'dedicated', 'group'].includes(updates.accountType)) {
return res
.status(400)
.json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
}
// 如果更新为分组类型验证groupId或groupIds
if (
updates.accountType === 'group' &&
!updates.groupId &&
(!updates.groupIds || updates.groupIds.length === 0)
) {
return res.status(400).json({ error: 'Group ID is required for group type accounts' })
}
// 获取账户当前信息以处理分组变更
const currentAccount = await geminiAccountService.getAccount(accountId)
if (!currentAccount) {
return res.status(404).json({ error: 'Account not found' })
}
// ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
const mappedUpdates = mapExpiryField(updates, 'Gemini', accountId)
// 处理分组的变更
if (mappedUpdates.accountType !== undefined) {
// 如果之前是分组类型,需要从所有分组中移除
if (currentAccount.accountType === 'group') {
const oldGroups = await accountGroupService.getAccountGroups(accountId)
for (const oldGroup of oldGroups) {
await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id)
}
}
// 如果新类型是分组,处理多分组支持
if (mappedUpdates.accountType === 'group') {
if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'groupIds')) {
// 如果明确提供了 groupIds 参数(包括空数组)
if (mappedUpdates.groupIds && mappedUpdates.groupIds.length > 0) {
// 设置新的多分组
await accountGroupService.setAccountGroups(accountId, mappedUpdates.groupIds, 'gemini')
} else {
// groupIds 为空数组,从所有分组中移除
await accountGroupService.removeAccountFromAllGroups(accountId)
}
} else if (mappedUpdates.groupId) {
// 向后兼容:仅当没有 groupIds 但有 groupId 时使用单分组逻辑
await accountGroupService.addAccountToGroup(accountId, mappedUpdates.groupId, 'gemini')
}
}
}
const updatedAccount = await geminiAccountService.updateAccount(accountId, mappedUpdates)
logger.success(`📝 Admin updated Gemini account: ${accountId}`)
return res.json({ success: true, data: updatedAccount })
} catch (error) {
logger.error('❌ Failed to update Gemini account:', error)
return res.status(500).json({ error: 'Failed to update account', message: error.message })
}
})
// 删除 Gemini 账户
router.delete('/:accountId', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
// 自动解绑所有绑定的 API Keys
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(accountId, 'gemini')
// 获取账户信息以检查是否在分组中
const account = await geminiAccountService.getAccount(accountId)
if (account && account.accountType === 'group') {
const groups = await accountGroupService.getAccountGroups(accountId)
for (const group of groups) {
await accountGroupService.removeAccountFromGroup(accountId, group.id)
}
}
await geminiAccountService.deleteAccount(accountId)
let message = 'Gemini账号已成功删除'
if (unboundCount > 0) {
message += `${unboundCount} 个 API Key 已切换为共享池模式`
}
logger.success(`🗑️ Admin deleted Gemini account: ${accountId}, unbound ${unboundCount} keys`)
return res.json({
success: true,
message,
unboundKeys: unboundCount
})
} catch (error) {
logger.error('❌ Failed to delete Gemini account:', error)
return res.status(500).json({ error: 'Failed to delete account', message: error.message })
}
})
// 刷新 Gemini 账户 token
router.post('/:accountId/refresh', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const result = await geminiAccountService.refreshAccountToken(accountId)
logger.success(`🔄 Admin refreshed token for Gemini account: ${accountId}`)
return res.json({ success: true, data: result })
} catch (error) {
logger.error('❌ Failed to refresh Gemini account token:', error)
return res.status(500).json({ error: 'Failed to refresh token', message: error.message })
}
})
// 切换 Gemini 账户调度状态
router.put('/:accountId/toggle-schedulable', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const account = await geminiAccountService.getAccount(accountId)
if (!account) {
return res.status(404).json({ error: 'Account not found' })
}
// 现在 account.schedulable 已经是布尔值了,直接取反即可
const newSchedulable = !account.schedulable
await geminiAccountService.updateAccount(accountId, { schedulable: String(newSchedulable) })
// 验证更新是否成功,重新获取账户信息
const updatedAccount = await geminiAccountService.getAccount(accountId)
const actualSchedulable = updatedAccount ? updatedAccount.schedulable : newSchedulable
// 如果账号被禁用发送webhook通知
if (!actualSchedulable) {
await webhookNotifier.sendAccountAnomalyNotification({
accountId: account.id,
accountName: account.accountName || 'Gemini Account',
platform: 'gemini',
status: 'disabled',
errorCode: 'GEMINI_MANUALLY_DISABLED',
reason: '账号已被管理员手动禁用调度',
timestamp: new Date().toISOString()
})
}
logger.success(
`🔄 Admin toggled Gemini account schedulable status: ${accountId} -> ${
actualSchedulable ? 'schedulable' : 'not schedulable'
}`
)
// 返回实际的数据库值,确保前端状态与后端一致
return res.json({ success: true, schedulable: actualSchedulable })
} catch (error) {
logger.error('❌ Failed to toggle Gemini account schedulable status:', error)
return res
.status(500)
.json({ error: 'Failed to toggle schedulable status', message: error.message })
}
})
// 重置 Gemini OAuth 账户限流状态
router.post('/:id/reset-rate-limit', authenticateAdmin, async (req, res) => {
try {
const { id } = req.params
await geminiAccountService.updateAccount(id, {
rateLimitedAt: '',
rateLimitStatus: '',
status: 'active',
errorMessage: ''
})
logger.info(`🔄 Admin manually reset rate limit for Gemini account ${id}`)
res.json({
success: true,
message: 'Rate limit reset successfully'
})
} catch (error) {
logger.error('Failed to reset Gemini account rate limit:', error)
res.status(500).json({
success: false,
error: error.message
})
}
})
// 重置 Gemini OAuth 账户状态(清除所有异常状态)
router.post('/:id/reset-status', authenticateAdmin, async (req, res) => {
try {
const { id } = req.params
const result = await geminiAccountService.resetAccountStatus(id)
logger.success(`✅ Admin reset status for Gemini account: ${id}`)
return res.json({ success: true, data: result })
} catch (error) {
logger.error('❌ Failed to reset Gemini account status:', error)
return res.status(500).json({ error: 'Failed to reset status', message: error.message })
}
})
module.exports = router