mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
510 lines
18 KiB
JavaScript
510 lines
18 KiB
JavaScript
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 账户管理
|
||
function getDefaultRedirectUri(oauthProvider) {
|
||
if (oauthProvider === 'antigravity') {
|
||
return process.env.ANTIGRAVITY_OAUTH_REDIRECT_URI || 'http://localhost:45462'
|
||
}
|
||
return process.env.GEMINI_OAUTH_REDIRECT_URI || 'https://codeassist.google.com/authcode'
|
||
}
|
||
|
||
// 生成 Gemini OAuth 授权 URL
|
||
router.post('/generate-auth-url', authenticateAdmin, async (req, res) => {
|
||
try {
|
||
const { state, proxy, oauthProvider } = req.body // 接收代理配置与OAuth Provider
|
||
|
||
const redirectUri = getDefaultRedirectUri(oauthProvider)
|
||
|
||
logger.info(`Generating Gemini OAuth URL with redirect_uri: ${redirectUri}`)
|
||
|
||
const {
|
||
authUrl,
|
||
state: authState,
|
||
codeVerifier,
|
||
redirectUri: finalRedirectUri,
|
||
oauthProvider: resolvedOauthProvider
|
||
} = await geminiAccountService.generateAuthUrl(state, redirectUri, proxy, oauthProvider)
|
||
|
||
// 创建 OAuth 会话,包含 codeVerifier 和代理配置
|
||
const sessionId = authState
|
||
await redis.setOAuthSession(sessionId, {
|
||
state: authState,
|
||
type: 'gemini',
|
||
redirectUri: finalRedirectUri,
|
||
codeVerifier, // 保存 PKCE code verifier
|
||
proxy: proxy || null, // 保存代理配置
|
||
oauthProvider: resolvedOauthProvider,
|
||
createdAt: new Date().toISOString()
|
||
})
|
||
|
||
logger.info(`Generated Gemini OAuth URL with session: ${sessionId}`)
|
||
return res.json({
|
||
success: true,
|
||
data: {
|
||
authUrl,
|
||
sessionId,
|
||
oauthProvider: resolvedOauthProvider
|
||
}
|
||
})
|
||
} 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, oauthProvider } = req.body
|
||
let resolvedOauthProvider = oauthProvider
|
||
|
||
if (!code) {
|
||
return res.status(400).json({ error: 'Authorization code is required' })
|
||
}
|
||
|
||
let redirectUri = getDefaultRedirectUri(resolvedOauthProvider)
|
||
let codeVerifier = null
|
||
let proxyConfig = null
|
||
|
||
// 如果提供了 sessionId,从 OAuth 会话中获取信息
|
||
if (sessionId) {
|
||
const sessionData = await redis.getOAuthSession(sessionId)
|
||
if (sessionData) {
|
||
const {
|
||
redirectUri: sessionRedirectUri,
|
||
codeVerifier: sessionCodeVerifier,
|
||
proxy,
|
||
oauthProvider: sessionOauthProvider
|
||
} = sessionData
|
||
redirectUri = sessionRedirectUri || redirectUri
|
||
codeVerifier = sessionCodeVerifier
|
||
proxyConfig = proxy // 获取代理配置
|
||
if (!resolvedOauthProvider && sessionOauthProvider) {
|
||
// 会话里保存的 provider 仅作为兜底
|
||
resolvedOauthProvider = sessionOauthProvider
|
||
}
|
||
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, // 传递代理配置
|
||
resolvedOauthProvider
|
||
)
|
||
|
||
// 清理 OAuth 会话
|
||
if (sessionId) {
|
||
await redis.deleteOAuthSession(sessionId)
|
||
}
|
||
|
||
logger.success('✅ Successfully exchanged Gemini authorization code')
|
||
return res.json({ success: true, data: { tokens, oauthProvider: resolvedOauthProvider } })
|
||
} 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
|