From bae39d5468e8525ce0a6108e31192a471ad7736e Mon Sep 17 00:00:00 2001 From: shaw Date: Sun, 23 Nov 2025 22:00:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81Gemini-Api=E6=8E=A5?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/admin.js | 405 ++++++++++++ src/routes/geminiRoutes.js | 357 +++++++++-- src/routes/standardGeminiRoutes.js | 415 +++++++++---- src/services/apiKeyService.js | 16 +- src/services/geminiApiAccountService.js | 586 ++++++++++++++++++ src/services/unifiedGeminiScheduler.js | 346 ++++++++--- .../src/components/accounts/AccountForm.vue | 202 +++++- .../accounts/AccountUsageDetailModal.vue | 1 + .../components/apikeys/EditApiKeyModal.vue | 49 +- .../src/components/common/AccountSelector.vue | 73 ++- web/admin-spa/src/stores/accounts.js | 42 ++ web/admin-spa/src/views/AccountsView.vue | 91 ++- web/admin-spa/src/views/ApiKeysView.vue | 59 +- 13 files changed, 2355 insertions(+), 287 deletions(-) create mode 100644 src/services/geminiApiAccountService.js diff --git a/src/routes/admin.js b/src/routes/admin.js index ae8395de..3cca87ed 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -4366,6 +4366,7 @@ router.get('/accounts/:accountId/usage-history', authenticateAdmin, async (req, 'openai', 'openai-responses', 'gemini', + 'gemini-api', 'droid' ] if (!allowedPlatforms.includes(platform)) { @@ -4378,6 +4379,7 @@ router.get('/accounts/:accountId/usage-history', authenticateAdmin, async (req, const accountTypeMap = { openai: 'openai', 'openai-responses': 'openai-responses', + 'gemini-api': 'gemini-api', droid: 'droid' } @@ -4387,6 +4389,7 @@ router.get('/accounts/:accountId/usage-history', authenticateAdmin, async (req, openai: 'gpt-4o-mini-2024-07-18', 'openai-responses': 'gpt-4o-mini-2024-07-18', gemini: 'gemini-1.5-flash', + 'gemini-api': 'gemini-2.0-flash', droid: 'unknown' } @@ -4411,6 +4414,11 @@ router.get('/accounts/:accountId/usage-history', authenticateAdmin, async (req, case 'gemini': accountData = await geminiAccountService.getAccount(accountId) break + case 'gemini-api': { + const geminiApiAccountService = require('../services/geminiApiAccountService') + accountData = await geminiApiAccountService.getAccount(accountId) + break + } case 'droid': accountData = await droidAccountService.getAccount(accountId) break @@ -9181,4 +9189,401 @@ router.post('/droid-accounts/:id/refresh-token', authenticateAdmin, async (req, } }) +// ==================== Gemini-API 账户管理 API ==================== + +// 获取所有 Gemini-API 账户 +router.get('/gemini-api-accounts', authenticateAdmin, async (req, res) => { + try { + const geminiApiAccountService = require('../services/geminiApiAccountService') + const { platform, groupId } = req.query + let accounts = await geminiApiAccountService.getAllAccounts(true) + + // 根据查询参数进行筛选 + if (platform && platform !== 'gemini-api') { + accounts = [] + } + + // 根据分组ID筛选 + if (groupId) { + const group = await accountGroupService.getGroup(groupId) + if (group && group.platform === 'gemini' && group.memberIds && group.memberIds.length > 0) { + accounts = accounts.filter((account) => group.memberIds.includes(account.id)) + } else { + accounts = [] + } + } + + // 处理使用统计和绑定的 API Key 数量 + const accountsWithStats = await Promise.all( + accounts.map(async (account) => { + // 检查并清除过期的限流状态 + await geminiApiAccountService.checkAndClearRateLimit(account.id) + + // 获取使用统计信息 + let usageStats + try { + usageStats = await redis.getAccountUsageStats(account.id, 'gemini-api') + } catch (error) { + logger.debug(`Failed to get usage stats for Gemini-API account ${account.id}:`, error) + usageStats = { + daily: { requests: 0, tokens: 0, allTokens: 0 }, + total: { requests: 0, tokens: 0, allTokens: 0 }, + monthly: { requests: 0, tokens: 0, allTokens: 0 } + } + } + + // 计算绑定的API Key数量(支持 api: 前缀) + const allKeys = await redis.getAllApiKeys() + let boundCount = 0 + + for (const key of allKeys) { + if (key.geminiAccountId) { + // 检查是否绑定了此 Gemini-API 账户(支持 api: 前缀) + if (key.geminiAccountId === `api:${account.id}`) { + boundCount++ + } + } + } + + return { + ...account, + usage: { + daily: usageStats.daily, + total: usageStats.total, + averages: usageStats.averages || usageStats.monthly + }, + boundApiKeys: boundCount + } + }) + ) + + res.json({ success: true, data: accountsWithStats }) + } catch (error) { + logger.error('Failed to get Gemini-API accounts:', error) + res.status(500).json({ success: false, message: error.message }) + } +}) + +// 创建 Gemini-API 账户 +router.post('/gemini-api-accounts', authenticateAdmin, async (req, res) => { + try { + const geminiApiAccountService = require('../services/geminiApiAccountService') + const { accountType, groupId, groupIds } = req.body + + // 验证accountType的有效性 + if (accountType && !['shared', 'dedicated', 'group'].includes(accountType)) { + return res.status(400).json({ + success: false, + error: 'Invalid account type. Must be "shared", "dedicated" or "group"' + }) + } + + // 如果是分组类型,验证groupId或groupIds + if (accountType === 'group' && !groupId && (!groupIds || groupIds.length === 0)) { + return res.status(400).json({ + success: false, + error: 'Group ID or Group IDs are required for group type accounts' + }) + } + + const account = await geminiApiAccountService.createAccount(req.body) + + // 如果是分组类型,将账户添加到分组 + if (accountType === 'group') { + if (groupIds && groupIds.length > 0) { + // 使用多分组设置 + await accountGroupService.setAccountGroups(account.id, groupIds, 'gemini') + } else if (groupId) { + // 兼容单分组模式 + await accountGroupService.addAccountToGroup(account.id, groupId, 'gemini') + } + } + + logger.success( + `🏢 Admin created new Gemini-API account: ${account.name} (${accountType || 'shared'})` + ) + + res.json({ success: true, data: account }) + } catch (error) { + logger.error('Failed to create Gemini-API account:', error) + res.status(500).json({ + success: false, + error: error.message + }) + } +}) + +// 获取单个 Gemini-API 账户 +router.get('/gemini-api-accounts/:id', authenticateAdmin, async (req, res) => { + try { + const geminiApiAccountService = require('../services/geminiApiAccountService') + const { id } = req.params + const account = await geminiApiAccountService.getAccount(id) + + if (!account) { + return res.status(404).json({ + success: false, + message: 'Account not found' + }) + } + + // 隐藏敏感信息 + account.apiKey = '***' + + res.json({ success: true, data: account }) + } catch (error) { + logger.error('Failed to get Gemini-API account:', error) + res.status(500).json({ + success: false, + error: error.message + }) + } +}) + +// 更新 Gemini-API 账户 +router.put('/gemini-api-accounts/:id', authenticateAdmin, async (req, res) => { + try { + const geminiApiAccountService = require('../services/geminiApiAccountService') + const { id } = req.params + const updates = req.body + + // 验证priority的有效性(1-100) + if (updates.priority !== undefined) { + const priority = parseInt(updates.priority) + if (isNaN(priority) || priority < 1 || priority > 100) { + return res.status(400).json({ + success: false, + message: 'Priority must be a number between 1 and 100' + }) + } + } + + // 验证accountType的有效性 + if (updates.accountType && !['shared', 'dedicated', 'group'].includes(updates.accountType)) { + return res.status(400).json({ + success: false, + 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({ + success: false, + error: 'Group ID or Group IDs are required for group type accounts' + }) + } + + // 获取账户当前信息以处理分组变更 + const currentAccount = await geminiApiAccountService.getAccount(id) + if (!currentAccount) { + return res.status(404).json({ + success: false, + error: 'Account not found' + }) + } + + // 处理分组的变更 + if (updates.accountType !== undefined) { + // 如果之前是分组类型,需要从所有分组中移除 + if (currentAccount.accountType === 'group') { + await accountGroupService.removeAccountFromAllGroups(id) + } + + // 如果新类型是分组,添加到新分组 + if (updates.accountType === 'group') { + // 处理多分组/单分组的兼容性 + if (Object.prototype.hasOwnProperty.call(updates, 'groupIds')) { + if (updates.groupIds && updates.groupIds.length > 0) { + // 使用多分组设置 + await accountGroupService.setAccountGroups(id, updates.groupIds, 'gemini') + } + } else if (updates.groupId) { + // 兼容单分组模式 + await accountGroupService.addAccountToGroup(id, updates.groupId, 'gemini') + } + } + } + + const result = await geminiApiAccountService.updateAccount(id, updates) + + if (!result.success) { + return res.status(400).json(result) + } + + logger.success(`📝 Admin updated Gemini-API account: ${currentAccount.name}`) + + res.json({ success: true, ...result }) + } catch (error) { + logger.error('Failed to update Gemini-API account:', error) + res.status(500).json({ + success: false, + error: error.message + }) + } +}) + +// 删除 Gemini-API 账户 +router.delete('/gemini-api-accounts/:id', authenticateAdmin, async (req, res) => { + try { + const geminiApiAccountService = require('../services/geminiApiAccountService') + const { id } = req.params + + const account = await geminiApiAccountService.getAccount(id) + if (!account) { + return res.status(404).json({ + success: false, + message: 'Account not found' + }) + } + + // 自动解绑所有绑定的 API Keys(支持 api: 前缀) + const unboundCount = await apiKeyService.unbindAccountFromAllKeys(id, 'gemini-api') + + // 检查是否在分组中 + const groups = await accountGroupService.getAllGroups() + for (const group of groups) { + if (group.platform === 'gemini' && group.memberIds && group.memberIds.includes(id)) { + await accountGroupService.removeMemberFromGroup(group.id, id) + logger.info(`Removed Gemini-API account ${id} from group ${group.id}`) + } + } + + const result = await geminiApiAccountService.deleteAccount(id) + + let message = 'Gemini-API账号已成功删除' + if (unboundCount > 0) { + message += `,${unboundCount} 个 API Key 已切换为共享池模式` + } + + logger.success(`✅ ${message}`) + + res.json({ + success: true, + ...result, + message, + unboundKeys: unboundCount + }) + } catch (error) { + logger.error('Failed to delete Gemini-API account:', error) + res.status(500).json({ + success: false, + error: error.message + }) + } +}) + +// 切换 Gemini-API 账户调度状态 +router.put('/gemini-api-accounts/:id/toggle-schedulable', authenticateAdmin, async (req, res) => { + try { + const geminiApiAccountService = require('../services/geminiApiAccountService') + const { id } = req.params + + const result = await geminiApiAccountService.toggleSchedulable(id) + + if (!result.success) { + return res.status(400).json(result) + } + + // 仅在停止调度时发送通知 + if (!result.schedulable) { + await webhookNotifier.sendAccountEvent('account.status_changed', { + accountId: id, + platform: 'gemini-api', + schedulable: result.schedulable, + changedBy: 'admin', + action: 'stopped_scheduling' + }) + } + + res.json(result) + } catch (error) { + logger.error('Failed to toggle Gemini-API account schedulable status:', error) + res.status(500).json({ + success: false, + error: error.message + }) + } +}) + +// 切换 Gemini-API 账户激活状态 +router.put('/gemini-api-accounts/:id/toggle', authenticateAdmin, async (req, res) => { + try { + const geminiApiAccountService = require('../services/geminiApiAccountService') + const { id } = req.params + + const account = await geminiApiAccountService.getAccount(id) + if (!account) { + return res.status(404).json({ + success: false, + message: 'Account not found' + }) + } + + const newActiveStatus = account.isActive === 'true' ? 'false' : 'true' + await geminiApiAccountService.updateAccount(id, { + isActive: newActiveStatus + }) + + res.json({ + success: true, + isActive: newActiveStatus === 'true' + }) + } catch (error) { + logger.error('Failed to toggle Gemini-API account status:', error) + res.status(500).json({ + success: false, + error: error.message + }) + } +}) + +// 重置 Gemini-API 账户限流状态 +router.post('/gemini-api-accounts/:id/reset-rate-limit', authenticateAdmin, async (req, res) => { + try { + const geminiApiAccountService = require('../services/geminiApiAccountService') + const { id } = req.params + + await geminiApiAccountService.updateAccount(id, { + rateLimitedAt: '', + rateLimitStatus: '', + status: 'active', + errorMessage: '' + }) + + logger.info(`🔄 Admin manually reset rate limit for Gemini-API account ${id}`) + + res.json({ + success: true, + message: 'Rate limit reset successfully' + }) + } catch (error) { + logger.error('Failed to reset Gemini-API account rate limit:', error) + res.status(500).json({ + success: false, + error: error.message + }) + } +}) + +// 重置 Gemini-API 账户状态(清除所有异常状态) +router.post('/gemini-api-accounts/:id/reset-status', authenticateAdmin, async (req, res) => { + try { + const geminiApiAccountService = require('../services/geminiApiAccountService') + const { id } = req.params + + const result = await geminiApiAccountService.resetAccountStatus(id) + + logger.success(`✅ Admin reset status for Gemini-API account: ${id}`) + return res.json({ success: true, data: result }) + } catch (error) { + logger.error('❌ Failed to reset Gemini-API account status:', error) + return res.status(500).json({ error: 'Failed to reset status', message: error.message }) + } +}) + module.exports = router diff --git a/src/routes/geminiRoutes.js b/src/routes/geminiRoutes.js index c9f9f244..3d74679d 100644 --- a/src/routes/geminiRoutes.js +++ b/src/routes/geminiRoutes.js @@ -3,6 +3,7 @@ const router = express.Router() const logger = require('../utils/logger') const { authenticateApiKey } = require('../middleware/auth') const geminiAccountService = require('../services/geminiAccountService') +const geminiApiAccountService = require('../services/geminiApiAccountService') const { sendGeminiRequest, getAvailableModels } = require('../services/geminiRelayService') const crypto = require('crypto') const sessionHelper = require('../utils/sessionHelper') @@ -10,6 +11,8 @@ const unifiedGeminiScheduler = require('../services/unifiedGeminiScheduler') const apiKeyService = require('../services/apiKeyService') const { updateRateLimitCounters } = require('../utils/rateLimitHelper') const { parseSSELine } = require('../utils/sseParser') +const axios = require('axios') +const ProxyHelper = require('../utils/proxyHelper') // const { OAuth2Client } = require('google-auth-library'); // OAuth2Client is not used in this file // 生成会话哈希 @@ -77,6 +80,9 @@ async function applyRateLimitTracking(req, usageSummary, model, context = '') { router.post('/messages', authenticateApiKey, async (req, res) => { const startTime = Date.now() let abortController = null + let accountId + let accountType + let sessionHash try { const apiKeyData = req.apiKey @@ -111,18 +117,17 @@ router.post('/messages', authenticateApiKey, async (req, res) => { } // 生成会话哈希用于粘性会话 - const sessionHash = generateSessionHash(req) + sessionHash = generateSessionHash(req) // 使用统一调度选择可用的 Gemini 账户(传递请求的模型) - let accountId try { const schedulerResult = await unifiedGeminiScheduler.selectAccountForApiKey( apiKeyData, sessionHash, - model // 传递请求的模型进行过滤 + model, // 传递请求的模型进行过滤 + { allowApiAccounts: true } // 允许调度 API 账户 ) - const { accountId: selectedAccountId } = schedulerResult - accountId = selectedAccountId + ;({ accountId, accountType } = schedulerResult) } catch (error) { logger.error('Failed to select Gemini account:', error) return res.status(503).json({ @@ -133,22 +138,39 @@ router.post('/messages', authenticateApiKey, async (req, res) => { }) } + // 判断账户类型:根据 accountType 判断,而非 accountId 前缀 + const isApiAccount = accountType === 'gemini-api' + // 获取账户详情 - const account = await geminiAccountService.getAccount(accountId) - if (!account) { - return res.status(503).json({ - error: { - message: 'Selected account not found', - type: 'service_unavailable' - } - }) + let account + if (isApiAccount) { + account = await geminiApiAccountService.getAccount(accountId) + if (!account) { + return res.status(503).json({ + error: { + message: 'Gemini API account not found', + type: 'service_unavailable' + } + }) + } + logger.info(`Using Gemini API account: ${account.id} for API key: ${apiKeyData.id}`) + // 标记 API 账户被使用 + await geminiApiAccountService.markAccountUsed(account.id) + } else { + account = await geminiAccountService.getAccount(accountId) + if (!account) { + return res.status(503).json({ + error: { + message: 'Gemini OAuth account not found', + type: 'service_unavailable' + } + }) + } + logger.info(`Using Gemini OAuth account: ${account.id} for API key: ${apiKeyData.id}`) + // 标记 OAuth 账户被使用 + await geminiAccountService.markAccountUsed(account.id) } - logger.info(`Using Gemini account: ${account.id} for API key: ${apiKeyData.id}`) - - // 标记账户被使用 - await geminiAccountService.markAccountUsed(account.id) - // 创建中止控制器 abortController = new AbortController() @@ -160,20 +182,126 @@ router.post('/messages', authenticateApiKey, async (req, res) => { } }) - // 发送请求到 Gemini - const geminiResponse = await sendGeminiRequest({ - messages, - model, - temperature, - maxTokens: max_tokens, - stream, - accessToken: account.accessToken, - proxy: account.proxy, - apiKeyId: apiKeyData.id, - signal: abortController.signal, - projectId: account.projectId, - accountId: account.id - }) + let geminiResponse + + if (isApiAccount) { + // API 账户:直接调用 Google Gemini API + // 转换 OpenAI 格式的 messages 为 Gemini 格式的 contents + const contents = messages.map((msg) => ({ + role: msg.role === 'assistant' ? 'model' : msg.role, + parts: [{ text: msg.content }] + })) + + const requestBody = { + contents, + generationConfig: { + temperature, + maxOutputTokens: max_tokens, + topP: 0.95, + topK: 40 + } + } + + // 解析代理配置 + let proxyConfig = null + if (account.proxy) { + try { + proxyConfig = + typeof account.proxy === 'string' ? JSON.parse(account.proxy) : account.proxy + } catch (e) { + logger.warn('Failed to parse proxy configuration:', e) + } + } + + const apiUrl = stream + ? `${account.baseUrl}/v1beta/models/${model}:streamGenerateContent?key=${account.apiKey}&alt=sse` + : `${account.baseUrl}/v1beta/models/${model}:generateContent?key=${account.apiKey}` + + const axiosConfig = { + method: 'POST', + url: apiUrl, + data: requestBody, + headers: { + 'Content-Type': 'application/json' + }, + responseType: stream ? 'stream' : 'json', + signal: abortController.signal + } + + // 添加代理配置 + if (proxyConfig) { + const proxyHelper = new ProxyHelper() + axiosConfig.httpsAgent = proxyHelper.createProxyAgent(proxyConfig) + axiosConfig.httpAgent = proxyHelper.createProxyAgent(proxyConfig) + } + + try { + const apiResponse = await axios(axiosConfig) + if (stream) { + geminiResponse = apiResponse.data + } else { + // 转换为 OpenAI 兼容格式 + const geminiData = apiResponse.data + geminiResponse = { + id: crypto.randomUUID(), + object: 'chat.completion', + created: Math.floor(Date.now() / 1000), + model, + choices: [ + { + index: 0, + message: { + role: 'assistant', + content: + geminiData.candidates?.[0]?.content?.parts?.[0]?.text || 'No response generated' + }, + finish_reason: 'stop' + } + ], + usage: { + prompt_tokens: geminiData.usageMetadata?.promptTokenCount || 0, + completion_tokens: geminiData.usageMetadata?.candidatesTokenCount || 0, + total_tokens: geminiData.usageMetadata?.totalTokenCount || 0 + } + } + + // 记录使用统计 + if (geminiData.usageMetadata) { + await apiKeyService.recordUsage( + apiKeyData.id, + geminiData.usageMetadata.promptTokenCount || 0, + geminiData.usageMetadata.candidatesTokenCount || 0, + 0, + 0, + model, + accountId // 使用原始 accountId(含 api: 前缀) + ) + } + } + } catch (error) { + logger.error('Gemini API request failed:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data + }) + throw error + } + } else { + // OAuth 账户:使用现有的 sendGeminiRequest + geminiResponse = await sendGeminiRequest({ + messages, + model, + temperature, + maxTokens: max_tokens, + stream, + accessToken: account.accessToken, + proxy: account.proxy, + apiKeyId: apiKeyData.id, + signal: abortController.signal, + projectId: account.projectId, + accountId: account.id + }) + } if (stream) { // 设置流式响应头 @@ -182,15 +310,90 @@ router.post('/messages', authenticateApiKey, async (req, res) => { res.setHeader('Connection', 'keep-alive') res.setHeader('X-Accel-Buffering', 'no') - // 流式传输响应 - for await (const chunk of geminiResponse) { - if (abortController.signal.aborted) { - break + if (isApiAccount) { + // API 账户:处理 SSE 流并记录使用统计 + let totalUsage = { + promptTokenCount: 0, + candidatesTokenCount: 0, + totalTokenCount: 0 } - res.write(chunk) - } - res.end() + geminiResponse.on('data', (chunk) => { + try { + const chunkStr = chunk.toString() + res.write(chunkStr) + + // 尝试从 SSE 流中提取 usage 数据 + const lines = chunkStr.split('\n') + for (const line of lines) { + if (line.startsWith('data:')) { + const data = line.substring(5).trim() + if (data && data !== '[DONE]') { + try { + const parsed = JSON.parse(data) + if (parsed.usageMetadata || parsed.response?.usageMetadata) { + totalUsage = parsed.usageMetadata || parsed.response.usageMetadata + } + } catch (e) { + // 解析失败,忽略 + } + } + } + } + } catch (error) { + logger.error('Error processing stream chunk:', error) + } + }) + + geminiResponse.on('end', () => { + res.end() + + // 异步记录使用统计 + if (totalUsage.totalTokenCount > 0) { + apiKeyService + .recordUsage( + apiKeyData.id, + totalUsage.promptTokenCount || 0, + totalUsage.candidatesTokenCount || 0, + 0, + 0, + model, + accountId // 使用原始 accountId(含 api: 前缀) + ) + .then(() => { + logger.info( + `📊 Recorded Gemini API stream usage - Input: ${totalUsage.promptTokenCount}, Output: ${totalUsage.candidatesTokenCount}` + ) + }) + .catch((error) => { + logger.error('Failed to record Gemini API usage:', error) + }) + } + }) + + geminiResponse.on('error', (error) => { + logger.error('Stream error:', error) + if (!res.headersSent) { + res.status(500).json({ + error: { + message: error.message || 'Stream error', + type: 'api_error' + } + }) + } else { + res.end() + } + }) + } else { + // OAuth 账户:使用原有的流式传输逻辑 + for await (const chunk of geminiResponse) { + if (abortController.signal.aborted) { + break + } + res.write(chunk) + } + res.end() + } } else { // 非流式响应 res.json(geminiResponse) @@ -202,14 +405,24 @@ router.post('/messages', authenticateApiKey, async (req, res) => { logger.error('Gemini request error:', error) // 处理速率限制 - if (error.status === 429) { - if (req.apiKey && req.account) { - await geminiAccountService.setAccountRateLimited(req.account.id, true) + const errorStatus = error.response?.status || error.status + if (errorStatus === 429 && accountId) { + try { + // 使用已有的 accountType 变量,而非检查前缀 + const rateLimitAccountType = accountType || 'gemini' + await unifiedGeminiScheduler.markAccountRateLimited( + accountId, + rateLimitAccountType, + sessionHash + ) + logger.warn(`⚠️ Gemini account ${accountId} rate limited (/messages), marking as limited`) + } catch (limitError) { + logger.warn('Failed to mark account as rate limited:', limitError) } } // 返回错误响应 - const status = error.status || 500 + const status = errorStatus || 500 const errorResponse = { error: error.error || { message: error.message || 'Internal server error', @@ -700,13 +913,38 @@ async function handleGenerateContent(req, res) { }) } - // 使用统一调度选择账号 - const { accountId } = await unifiedGeminiScheduler.selectAccountForApiKey( + // 使用统一调度选择账号(v1internal 不允许 API 账户) + const schedulerResult = await unifiedGeminiScheduler.selectAccountForApiKey( req.apiKey, sessionHash, model + // 不传 allowApiAccounts: true,所以不会调度 API 账户 ) + const { accountId, accountType } = schedulerResult + + // v1internal 路由只支持 OAuth 账户,不支持 API Key 账户 + if (accountType === 'gemini-api') { + logger.error(`❌ v1internal routes do not support Gemini API accounts. Account: ${accountId}`) + return res.status(400).json({ + error: { + message: + 'This endpoint only supports Gemini OAuth accounts. Gemini API Key accounts are not compatible with v1internal format.', + type: 'invalid_account_type' + } + }) + } + const account = await geminiAccountService.getAccount(accountId) + if (!account) { + logger.error(`❌ Gemini account not found: ${accountId}`) + return res.status(404).json({ + error: { + message: 'Gemini account not found', + type: 'account_not_found' + } + }) + } + const { accessToken, refreshToken } = account const version = req.path.includes('v1beta') ? 'v1beta' : 'v1internal' @@ -854,13 +1092,38 @@ async function handleStreamGenerateContent(req, res) { }) } - // 使用统一调度选择账号 - const { accountId } = await unifiedGeminiScheduler.selectAccountForApiKey( + // 使用统一调度选择账号(v1internal 不允许 API 账户) + const schedulerResult = await unifiedGeminiScheduler.selectAccountForApiKey( req.apiKey, sessionHash, model + // 不传 allowApiAccounts: true,所以不会调度 API 账户 ) + const { accountId, accountType } = schedulerResult + + // v1internal 路由只支持 OAuth 账户,不支持 API Key 账户 + if (accountType === 'gemini-api') { + logger.error(`❌ v1internal routes do not support Gemini API accounts. Account: ${accountId}`) + return res.status(400).json({ + error: { + message: + 'This endpoint only supports Gemini OAuth accounts. Gemini API Key accounts are not compatible with v1internal format.', + type: 'invalid_account_type' + } + }) + } + const account = await geminiAccountService.getAccount(accountId) + if (!account) { + logger.error(`❌ Gemini account not found: ${accountId}`) + return res.status(404).json({ + error: { + message: 'Gemini account not found', + type: 'account_not_found' + } + }) + } + const { accessToken, refreshToken } = account const version = req.path.includes('v1beta') ? 'v1beta' : 'v1internal' diff --git a/src/routes/standardGeminiRoutes.js b/src/routes/standardGeminiRoutes.js index 759e29b3..9e78c62b 100644 --- a/src/routes/standardGeminiRoutes.js +++ b/src/routes/standardGeminiRoutes.js @@ -3,9 +3,12 @@ const router = express.Router() const { authenticateApiKey } = require('../middleware/auth') const logger = require('../utils/logger') const geminiAccountService = require('../services/geminiAccountService') +const geminiApiAccountService = require('../services/geminiApiAccountService') const unifiedGeminiScheduler = require('../services/unifiedGeminiScheduler') const apiKeyService = require('../services/apiKeyService') const sessionHelper = require('../utils/sessionHelper') +const axios = require('axios') +const ProxyHelper = require('../utils/proxyHelper') // 导入 geminiRoutes 中导出的处理函数 const { handleLoadCodeAssist, handleOnboardUser, handleCountTokens } = require('./geminiRoutes') @@ -136,6 +139,8 @@ async function normalizeAxiosStreamError(error) { async function handleStandardGenerateContent(req, res) { let account = null let sessionHash = null + let accountId = null // 提升到外部作用域 + let isApiAccount = false // 提升到外部作用域 try { if (!ensureGeminiPermission(req, res)) { @@ -210,20 +215,48 @@ async function handleStandardGenerateContent(req, res) { } // 使用统一调度选择账号 - const { accountId } = await unifiedGeminiScheduler.selectAccountForApiKey( + const schedulerResult = await unifiedGeminiScheduler.selectAccountForApiKey( req.apiKey, sessionHash, - model + model, + { allowApiAccounts: true } // 允许调度 API 账户 ) - account = await geminiAccountService.getAccount(accountId) - const { accessToken, refreshToken } = account + ;({ accountId } = schedulerResult) + const { accountType } = schedulerResult + + // 判断账户类型:根据 accountType 判断,而非 accountId 前缀 + isApiAccount = accountType === 'gemini-api' // 赋值而不是声明 + const actualAccountId = accountId // accountId 已经是实际 ID,无需处理前缀 const version = req.path.includes('v1beta') ? 'v1beta' : 'v1' - logger.info(`Standard Gemini API generateContent request (${version})`, { - model, - projectId: account.projectId, - apiKeyId: req.apiKey?.id || 'unknown' - }) + + if (isApiAccount) { + // Gemini API 账户:使用 API Key 直接请求 + account = await geminiApiAccountService.getAccount(actualAccountId) + if (!account) { + return res.status(404).json({ + error: { + message: 'Gemini API account not found', + type: 'account_not_found' + } + }) + } + + logger.info(`Standard Gemini API generateContent request (${version}) - API Key Account`, { + model, + accountId: actualAccountId, + apiKeyId: req.apiKey?.id || 'unknown' + }) + } else { + // OAuth 账户:使用原有流程 + account = await geminiAccountService.getAccount(actualAccountId) + + logger.info(`Standard Gemini API generateContent request (${version}) - OAuth Account`, { + model, + projectId: account.projectId, + apiKeyId: req.apiKey?.id || 'unknown' + }) + } // 解析账户的代理配置 let proxyConfig = null @@ -235,63 +268,106 @@ async function handleStandardGenerateContent(req, res) { } } - const client = await geminiAccountService.getOauthClient(accessToken, refreshToken, proxyConfig) + let response - // 项目ID优先级:账户配置的项目ID > 临时项目ID > 尝试获取 - let effectiveProjectId = account.projectId || account.tempProjectId || null + if (isApiAccount) { + // Gemini API 账户:直接使用 API Key 请求 + // baseUrl 填写域名,如 https://generativelanguage.googleapis.com,版本固定为 v1beta + const apiUrl = `${account.baseUrl}/v1beta/models/${model}:generateContent?key=${account.apiKey}` - // 如果没有任何项目ID,尝试调用 loadCodeAssist 获取 - if (!effectiveProjectId) { - try { - logger.info('📋 No projectId available, attempting to fetch from loadCodeAssist...') - const loadResponse = await geminiAccountService.loadCodeAssist(client, null, proxyConfig) - - if (loadResponse.cloudaicompanionProject) { - effectiveProjectId = loadResponse.cloudaicompanionProject - // 保存临时项目ID - await geminiAccountService.updateTempProjectId(accountId, effectiveProjectId) - logger.info(`📋 Fetched and cached temporary projectId: ${effectiveProjectId}`) + // 构建 Axios 配置 + const axiosConfig = { + method: 'POST', + url: apiUrl, + data: actualRequestData, + headers: { + 'Content-Type': 'application/json' } - } catch (loadError) { - logger.warn('Failed to fetch projectId from loadCodeAssist:', loadError.message) } - } - // 如果还是没有项目ID,返回错误 - if (!effectiveProjectId) { - return res.status(403).json({ - error: { - message: - 'This account requires a project ID to be configured. Please configure a project ID in the account settings.', - type: 'configuration_required' + // 添加代理配置 + if (proxyConfig) { + const proxyHelper = new ProxyHelper() + axiosConfig.httpsAgent = proxyHelper.createProxyAgent(proxyConfig) + axiosConfig.httpAgent = proxyHelper.createProxyAgent(proxyConfig) + } + + try { + const apiResponse = await axios(axiosConfig) + response = { response: apiResponse.data } + } catch (error) { + logger.error('Gemini API request failed:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data + }) + throw error + } + } else { + // OAuth 账户:使用原有流程 + const { accessToken, refreshToken } = account + const client = await geminiAccountService.getOauthClient( + accessToken, + refreshToken, + proxyConfig + ) + + // 项目ID优先级:账户配置的项目ID > 临时项目ID > 尝试获取 + let effectiveProjectId = account.projectId || account.tempProjectId || null + + // 如果没有任何项目ID,尝试调用 loadCodeAssist 获取 + if (!effectiveProjectId) { + try { + logger.info('📋 No projectId available, attempting to fetch from loadCodeAssist...') + const loadResponse = await geminiAccountService.loadCodeAssist(client, null, proxyConfig) + + if (loadResponse.cloudaicompanionProject) { + effectiveProjectId = loadResponse.cloudaicompanionProject + // 保存临时项目ID + await geminiAccountService.updateTempProjectId(actualAccountId, effectiveProjectId) + logger.info(`📋 Fetched and cached temporary projectId: ${effectiveProjectId}`) + } + } catch (loadError) { + logger.warn('Failed to fetch projectId from loadCodeAssist:', loadError.message) } + } + + // 如果还是没有项目ID,返回错误 + if (!effectiveProjectId) { + return res.status(403).json({ + error: { + message: + 'This account requires a project ID to be configured. Please configure a project ID in the account settings.', + type: 'configuration_required' + } + }) + } + + logger.info('📋 Standard API 项目ID处理逻辑', { + accountProjectId: account.projectId, + tempProjectId: account.tempProjectId, + effectiveProjectId, + decision: account.projectId + ? '使用账户配置' + : account.tempProjectId + ? '使用临时项目ID' + : '从loadCodeAssist获取' }) + + // 生成一个符合 Gemini CLI 格式的 user_prompt_id + const userPromptId = `${require('crypto').randomUUID()}########0` + + // 调用内部 API(cloudcode-pa) + response = await geminiAccountService.generateContent( + client, + { model, request: actualRequestData }, + userPromptId, // 使用生成的 user_prompt_id + effectiveProjectId, // 使用处理后的项目ID + req.apiKey?.id, // 使用 API Key ID 作为 session ID + proxyConfig + ) } - logger.info('📋 Standard API 项目ID处理逻辑', { - accountProjectId: account.projectId, - tempProjectId: account.tempProjectId, - effectiveProjectId, - decision: account.projectId - ? '使用账户配置' - : account.tempProjectId - ? '使用临时项目ID' - : '从loadCodeAssist获取' - }) - - // 生成一个符合 Gemini CLI 格式的 user_prompt_id - const userPromptId = `${require('crypto').randomUUID()}########0` - - // 调用内部 API(cloudcode-pa) - const response = await geminiAccountService.generateContent( - client, - { model, request: actualRequestData }, - userPromptId, // 使用生成的 user_prompt_id - effectiveProjectId, // 使用处理后的项目ID - req.apiKey?.id, // 使用 API Key ID 作为 session ID - proxyConfig - ) - // 记录使用统计 if (response?.response?.usageMetadata) { try { @@ -303,7 +379,7 @@ async function handleStandardGenerateContent(req, res) { 0, // cacheCreateTokens 0, // cacheReadTokens model, - account.id + accountId // 账户 ID ) logger.info( `📊 Recorded Gemini usage - Input: ${usage.promptTokenCount}, Output: ${usage.candidatesTokenCount}, Total: ${usage.totalTokenCount}` @@ -327,10 +403,15 @@ async function handleStandardGenerateContent(req, res) { }) // 处理速率限制 - if (error.response?.status === 429) { - logger.warn(`⚠️ Gemini account ${account.id} rate limited (Standard API), marking as limited`) + if (error.response?.status === 429 && accountId) { + logger.warn(`⚠️ Gemini account ${accountId} rate limited (Standard API), marking as limited`) try { - await unifiedGeminiScheduler.markAccountRateLimited(account.id, 'gemini', sessionHash) + const rateLimitAccountType = isApiAccount ? 'gemini-api' : 'gemini' + await unifiedGeminiScheduler.markAccountRateLimited( + accountId, // 账户 ID + rateLimitAccountType, + sessionHash + ) } catch (limitError) { logger.warn('Failed to mark account as rate limited in scheduler:', limitError) } @@ -350,6 +431,8 @@ async function handleStandardStreamGenerateContent(req, res) { let abortController = null let account = null let sessionHash = null + let accountId = null // 提升到外部作用域 + let isApiAccount = false // 提升到外部作用域 try { if (!ensureGeminiPermission(req, res)) { @@ -424,20 +507,54 @@ async function handleStandardStreamGenerateContent(req, res) { } // 使用统一调度选择账号 - const { accountId } = await unifiedGeminiScheduler.selectAccountForApiKey( + const schedulerResult = await unifiedGeminiScheduler.selectAccountForApiKey( req.apiKey, sessionHash, - model + model, + { allowApiAccounts: true } // 允许调度 API 账户 ) - account = await geminiAccountService.getAccount(accountId) - const { accessToken, refreshToken } = account + ;({ accountId } = schedulerResult) + const { accountType } = schedulerResult + + // 判断账户类型:根据 accountType 判断,而非 accountId 前缀 + isApiAccount = accountType === 'gemini-api' // 赋值而不是声明 + const actualAccountId = accountId // accountId 已经是实际 ID,无需处理前缀 const version = req.path.includes('v1beta') ? 'v1beta' : 'v1' - logger.info(`Standard Gemini API streamGenerateContent request (${version})`, { - model, - projectId: account.projectId, - apiKeyId: req.apiKey?.id || 'unknown' - }) + + if (isApiAccount) { + // Gemini API 账户:使用 API Key 直接请求 + account = await geminiApiAccountService.getAccount(actualAccountId) + if (!account) { + return res.status(404).json({ + error: { + message: 'Gemini API account not found', + type: 'account_not_found' + } + }) + } + + logger.info( + `Standard Gemini API streamGenerateContent request (${version}) - API Key Account`, + { + model, + accountId: actualAccountId, + apiKeyId: req.apiKey?.id || 'unknown' + } + ) + } else { + // OAuth 账户:使用原有流程 + account = await geminiAccountService.getAccount(actualAccountId) + + logger.info( + `Standard Gemini API streamGenerateContent request (${version}) - OAuth Account`, + { + model, + projectId: account.projectId, + apiKeyId: req.apiKey?.id || 'unknown' + } + ) + } // 创建中止控制器 abortController = new AbortController() @@ -460,64 +577,109 @@ async function handleStandardStreamGenerateContent(req, res) { } } - const client = await geminiAccountService.getOauthClient(accessToken, refreshToken, proxyConfig) + let streamResponse - // 项目ID优先级:账户配置的项目ID > 临时项目ID > 尝试获取 - let effectiveProjectId = account.projectId || account.tempProjectId || null + if (isApiAccount) { + // Gemini API 账户:直接使用 API Key 请求流式接口 + // baseUrl 填写域名,版本固定为 v1beta + const apiUrl = `${account.baseUrl}/v1beta/models/${model}:streamGenerateContent?key=${account.apiKey}&alt=sse` - // 如果没有任何项目ID,尝试调用 loadCodeAssist 获取 - if (!effectiveProjectId) { - try { - logger.info('📋 No projectId available, attempting to fetch from loadCodeAssist...') - const loadResponse = await geminiAccountService.loadCodeAssist(client, null, proxyConfig) - - if (loadResponse.cloudaicompanionProject) { - effectiveProjectId = loadResponse.cloudaicompanionProject - // 保存临时项目ID - await geminiAccountService.updateTempProjectId(accountId, effectiveProjectId) - logger.info(`📋 Fetched and cached temporary projectId: ${effectiveProjectId}`) - } - } catch (loadError) { - logger.warn('Failed to fetch projectId from loadCodeAssist:', loadError.message) + // 构建 Axios 配置 + const axiosConfig = { + method: 'POST', + url: apiUrl, + data: actualRequestData, + headers: { + 'Content-Type': 'application/json' + }, + responseType: 'stream', + signal: abortController.signal } - } - // 如果还是没有项目ID,返回错误 - if (!effectiveProjectId) { - return res.status(403).json({ - error: { - message: - 'This account requires a project ID to be configured. Please configure a project ID in the account settings.', - type: 'configuration_required' + // 添加代理配置 + if (proxyConfig) { + const proxyHelper = new ProxyHelper() + axiosConfig.httpsAgent = proxyHelper.createProxyAgent(proxyConfig) + axiosConfig.httpAgent = proxyHelper.createProxyAgent(proxyConfig) + } + + try { + const apiResponse = await axios(axiosConfig) + streamResponse = apiResponse.data + } catch (error) { + logger.error('Gemini API stream request failed:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data + }) + throw error + } + } else { + // OAuth 账户:使用原有流程 + const { accessToken, refreshToken } = account + const client = await geminiAccountService.getOauthClient( + accessToken, + refreshToken, + proxyConfig + ) + + // 项目ID优先级:账户配置的项目ID > 临时项目ID > 尝试获取 + let effectiveProjectId = account.projectId || account.tempProjectId || null + + // 如果没有任何项目ID,尝试调用 loadCodeAssist 获取 + if (!effectiveProjectId) { + try { + logger.info('📋 No projectId available, attempting to fetch from loadCodeAssist...') + const loadResponse = await geminiAccountService.loadCodeAssist(client, null, proxyConfig) + + if (loadResponse.cloudaicompanionProject) { + effectiveProjectId = loadResponse.cloudaicompanionProject + // 保存临时项目ID + await geminiAccountService.updateTempProjectId(actualAccountId, effectiveProjectId) + logger.info(`📋 Fetched and cached temporary projectId: ${effectiveProjectId}`) + } + } catch (loadError) { + logger.warn('Failed to fetch projectId from loadCodeAssist:', loadError.message) } + } + + // 如果还是没有项目ID,返回错误 + if (!effectiveProjectId) { + return res.status(403).json({ + error: { + message: + 'This account requires a project ID to be configured. Please configure a project ID in the account settings.', + type: 'configuration_required' + } + }) + } + + logger.info('📋 Standard API 流式项目ID处理逻辑', { + accountProjectId: account.projectId, + tempProjectId: account.tempProjectId, + effectiveProjectId, + decision: account.projectId + ? '使用账户配置' + : account.tempProjectId + ? '使用临时项目ID' + : '从loadCodeAssist获取' }) + + // 生成一个符合 Gemini CLI 格式的 user_prompt_id + const userPromptId = `${require('crypto').randomUUID()}########0` + + // 调用内部 API(cloudcode-pa)的流式接口 + streamResponse = await geminiAccountService.generateContentStream( + client, + { model, request: actualRequestData }, + userPromptId, // 使用生成的 user_prompt_id + effectiveProjectId, // 使用处理后的项目ID + req.apiKey?.id, // 使用 API Key ID 作为 session ID + abortController.signal, + proxyConfig + ) } - logger.info('📋 Standard API 流式项目ID处理逻辑', { - accountProjectId: account.projectId, - tempProjectId: account.tempProjectId, - effectiveProjectId, - decision: account.projectId - ? '使用账户配置' - : account.tempProjectId - ? '使用临时项目ID' - : '从loadCodeAssist获取' - }) - - // 生成一个符合 Gemini CLI 格式的 user_prompt_id - const userPromptId = `${require('crypto').randomUUID()}########0` - - // 调用内部 API(cloudcode-pa)的流式接口 - const streamResponse = await geminiAccountService.generateContentStream( - client, - { model, request: actualRequestData }, - userPromptId, // 使用生成的 user_prompt_id - effectiveProjectId, // 使用处理后的项目ID - req.apiKey?.id, // 使用 API Key ID 作为 session ID - abortController.signal, - proxyConfig - ) - // 设置 SSE 响应头 res.setHeader('Content-Type', 'text/event-stream') res.setHeader('Cache-Control', 'no-cache') @@ -672,7 +834,7 @@ async function handleStandardStreamGenerateContent(req, res) { 0, // cacheCreateTokens 0, // cacheReadTokens model, - account.id + accountId // 使用原始 accountId(含前缀) ) .then(() => { logger.info( @@ -743,12 +905,17 @@ async function handleStandardStreamGenerateContent(req, res) { }) // 处理速率限制 - if (error.response?.status === 429) { + if (error.response?.status === 429 && accountId) { logger.warn( - `⚠️ Gemini account ${account.id} rate limited (Standard Stream API), marking as limited` + `⚠️ Gemini account ${accountId} rate limited (Standard Stream API), marking as limited` ) try { - await unifiedGeminiScheduler.markAccountRateLimited(account.id, 'gemini', sessionHash) + const rateLimitAccountType = isApiAccount ? 'gemini-api' : 'gemini' + await unifiedGeminiScheduler.markAccountRateLimited( + accountId, // 账户 ID + rateLimitAccountType, + sessionHash + ) } catch (limitError) { logger.warn('Failed to mark account as rate limited in scheduler:', limitError) } diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index 4d439ad3..98fb2eaf 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -1527,7 +1527,15 @@ class ApiKeyService { permissions: keyData.permissions, dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0), totalCostLimit: parseFloat(keyData.totalCostLimit || 0), - droidAccountId: keyData.droidAccountId + // 所有平台账户绑定字段 + claudeAccountId: keyData.claudeAccountId, + claudeConsoleAccountId: keyData.claudeConsoleAccountId, + geminiAccountId: keyData.geminiAccountId, + openaiAccountId: keyData.openaiAccountId, + bedrockAccountId: keyData.bedrockAccountId, + droidAccountId: keyData.droidAccountId, + azureOpenaiAccountId: keyData.azureOpenaiAccountId, + ccrAccountId: keyData.ccrAccountId } } catch (error) { logger.error('❌ Failed to get API key by ID:', error) @@ -1670,6 +1678,7 @@ class ApiKeyService { claude: 'claudeAccountId', 'claude-console': 'claudeConsoleAccountId', gemini: 'geminiAccountId', + 'gemini-api': 'geminiAccountId', // 特殊处理,带 api: 前缀 openai: 'openaiAccountId', 'openai-responses': 'openaiAccountId', // 特殊处理,带 responses: 前缀 azure_openai: 'azureOpenaiAccountId', @@ -1692,6 +1701,9 @@ class ApiKeyService { if (accountType === 'openai-responses') { // OpenAI-Responses 特殊处理:查找 openaiAccountId 字段中带 responses: 前缀的 boundKeys = allKeys.filter((key) => key.openaiAccountId === `responses:${accountId}`) + } else if (accountType === 'gemini-api') { + // Gemini-API 特殊处理:查找 geminiAccountId 字段中带 api: 前缀的 + boundKeys = allKeys.filter((key) => key.geminiAccountId === `api:${accountId}`) } else { // 其他账号类型正常匹配 boundKeys = allKeys.filter((key) => key[field] === accountId) @@ -1702,6 +1714,8 @@ class ApiKeyService { const updates = {} if (accountType === 'openai-responses') { updates.openaiAccountId = null + } else if (accountType === 'gemini-api') { + updates.geminiAccountId = null } else if (accountType === 'claude-console') { updates.claudeConsoleAccountId = null } else { diff --git a/src/services/geminiApiAccountService.js b/src/services/geminiApiAccountService.js new file mode 100644 index 00000000..c03568a2 --- /dev/null +++ b/src/services/geminiApiAccountService.js @@ -0,0 +1,586 @@ +const { v4: uuidv4 } = require('uuid') +const crypto = require('crypto') +const redis = require('../models/redis') +const logger = require('../utils/logger') +const config = require('../../config/config') +const LRUCache = require('../utils/lruCache') + +class GeminiApiAccountService { + constructor() { + // 加密相关常量 + this.ENCRYPTION_ALGORITHM = 'aes-256-cbc' + this.ENCRYPTION_SALT = 'gemini-api-salt' + + // Redis 键前缀 + this.ACCOUNT_KEY_PREFIX = 'gemini_api_account:' + this.SHARED_ACCOUNTS_KEY = 'shared_gemini_api_accounts' + + // 🚀 性能优化:缓存派生的加密密钥,避免每次重复计算 + this._encryptionKeyCache = null + + // 🔄 解密结果缓存,提高解密性能 + this._decryptCache = new LRUCache(500) + + // 🧹 定期清理缓存(每10分钟) + setInterval( + () => { + this._decryptCache.cleanup() + logger.info('🧹 Gemini-API decrypt cache cleanup completed', this._decryptCache.getStats()) + }, + 10 * 60 * 1000 + ) + } + + // 创建账户 + async createAccount(options = {}) { + const { + name = 'Gemini API Account', + description = '', + apiKey = '', // 必填:Google AI Studio API Key + baseUrl = 'https://generativelanguage.googleapis.com', // 默认 Gemini API 基础 URL + proxy = null, + priority = 50, // 调度优先级 (1-100) + isActive = true, + accountType = 'shared', // 'dedicated' or 'shared' + schedulable = true, // 是否可被调度 + supportedModels = [], // 支持的模型列表 + rateLimitDuration = 60 // 限流时间(分钟) + } = options + + // 验证必填字段 + if (!apiKey) { + throw new Error('API Key is required for Gemini-API account') + } + + // 规范化 baseUrl(确保不以 / 结尾) + const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl + + const accountId = uuidv4() + + const accountData = { + id: accountId, + platform: 'gemini-api', + name, + description, + baseUrl: normalizedBaseUrl, + apiKey: this._encryptSensitiveData(apiKey), + priority: priority.toString(), + proxy: proxy ? JSON.stringify(proxy) : '', + isActive: isActive.toString(), + accountType, + schedulable: schedulable.toString(), + supportedModels: JSON.stringify(supportedModels), + + createdAt: new Date().toISOString(), + lastUsedAt: '', + status: 'active', + errorMessage: '', + + // 限流相关 + rateLimitedAt: '', + rateLimitStatus: '', + rateLimitDuration: rateLimitDuration.toString() + } + + // 保存到 Redis + await this._saveAccount(accountId, accountData) + + logger.success(`🚀 Created Gemini-API account: ${name} (${accountId})`) + + return { + ...accountData, + apiKey: '***' // 返回时隐藏敏感信息 + } + } + + // 获取账户 + async getAccount(accountId) { + const client = redis.getClientSafe() + const key = `${this.ACCOUNT_KEY_PREFIX}${accountId}` + const accountData = await client.hgetall(key) + + if (!accountData || !accountData.id) { + return null + } + + // 解密敏感数据 + accountData.apiKey = this._decryptSensitiveData(accountData.apiKey) + + // 解析 JSON 字段 + if (accountData.proxy) { + try { + accountData.proxy = JSON.parse(accountData.proxy) + } catch (e) { + accountData.proxy = null + } + } + + if (accountData.supportedModels) { + try { + accountData.supportedModels = JSON.parse(accountData.supportedModels) + } catch (e) { + accountData.supportedModels = [] + } + } + + return accountData + } + + // 更新账户 + async updateAccount(accountId, updates) { + const account = await this.getAccount(accountId) + if (!account) { + throw new Error('Account not found') + } + + // 处理敏感字段加密 + if (updates.apiKey) { + updates.apiKey = this._encryptSensitiveData(updates.apiKey) + } + + // 处理 JSON 字段 + if (updates.proxy !== undefined) { + updates.proxy = updates.proxy ? JSON.stringify(updates.proxy) : '' + } + + if (updates.supportedModels !== undefined) { + updates.supportedModels = JSON.stringify(updates.supportedModels) + } + + // 规范化 baseUrl + if (updates.baseUrl) { + updates.baseUrl = updates.baseUrl.endsWith('/') + ? updates.baseUrl.slice(0, -1) + : updates.baseUrl + } + + // 更新 Redis + const client = redis.getClientSafe() + const key = `${this.ACCOUNT_KEY_PREFIX}${accountId}` + await client.hset(key, updates) + + logger.info(`📝 Updated Gemini-API account: ${account.name}`) + + return { success: true } + } + + // 删除账户 + async deleteAccount(accountId) { + const client = redis.getClientSafe() + const key = `${this.ACCOUNT_KEY_PREFIX}${accountId}` + + // 从共享账户列表中移除 + await client.srem(this.SHARED_ACCOUNTS_KEY, accountId) + + // 删除账户数据 + await client.del(key) + + logger.info(`🗑️ Deleted Gemini-API account: ${accountId}`) + + return { success: true } + } + + // 获取所有账户 + async getAllAccounts(includeInactive = false) { + const client = redis.getClientSafe() + const accountIds = await client.smembers(this.SHARED_ACCOUNTS_KEY) + const accounts = [] + + for (const accountId of accountIds) { + const account = await this.getAccount(accountId) + if (account) { + // 过滤非活跃账户 + if (includeInactive || account.isActive === 'true') { + // 隐藏敏感信息 + account.apiKey = '***' + + // 获取限流状态信息 + const rateLimitInfo = this._getRateLimitInfo(account) + + // 格式化 rateLimitStatus 为对象 + account.rateLimitStatus = rateLimitInfo.isRateLimited + ? { + isRateLimited: true, + rateLimitedAt: account.rateLimitedAt || null, + minutesRemaining: rateLimitInfo.remainingMinutes || 0 + } + : { + isRateLimited: false, + rateLimitedAt: null, + minutesRemaining: 0 + } + + // 转换 schedulable 字段为布尔值 + account.schedulable = account.schedulable !== 'false' + // 转换 isActive 字段为布尔值 + account.isActive = account.isActive === 'true' + + account.platform = account.platform || 'gemini-api' + + accounts.push(account) + } + } + } + + // 直接从 Redis 获取所有账户(包括非共享账户) + const keys = await client.keys(`${this.ACCOUNT_KEY_PREFIX}*`) + for (const key of keys) { + const accountId = key.replace(this.ACCOUNT_KEY_PREFIX, '') + if (!accountIds.includes(accountId)) { + const accountData = await client.hgetall(key) + if (accountData && accountData.id) { + // 过滤非活跃账户 + if (includeInactive || accountData.isActive === 'true') { + // 隐藏敏感信息 + accountData.apiKey = '***' + + // 解析 JSON 字段 + if (accountData.proxy) { + try { + accountData.proxy = JSON.parse(accountData.proxy) + } catch (e) { + accountData.proxy = null + } + } + + if (accountData.supportedModels) { + try { + accountData.supportedModels = JSON.parse(accountData.supportedModels) + } catch (e) { + accountData.supportedModels = [] + } + } + + // 获取限流状态信息 + const rateLimitInfo = this._getRateLimitInfo(accountData) + + // 格式化 rateLimitStatus 为对象 + accountData.rateLimitStatus = rateLimitInfo.isRateLimited + ? { + isRateLimited: true, + rateLimitedAt: accountData.rateLimitedAt || null, + minutesRemaining: rateLimitInfo.remainingMinutes || 0 + } + : { + isRateLimited: false, + rateLimitedAt: null, + minutesRemaining: 0 + } + + // 转换 schedulable 字段为布尔值 + accountData.schedulable = accountData.schedulable !== 'false' + // 转换 isActive 字段为布尔值 + accountData.isActive = accountData.isActive === 'true' + + accountData.platform = accountData.platform || 'gemini-api' + + accounts.push(accountData) + } + } + } + } + + return accounts + } + + // 标记账户已使用 + async markAccountUsed(accountId) { + await this.updateAccount(accountId, { + lastUsedAt: new Date().toISOString() + }) + } + + // 标记账户限流 + async setAccountRateLimited(accountId, isLimited, duration = null) { + const account = await this.getAccount(accountId) + if (!account) { + return + } + + if (isLimited) { + const rateLimitDuration = duration || parseInt(account.rateLimitDuration) || 60 + const now = new Date() + const resetAt = new Date(now.getTime() + rateLimitDuration * 60000) + + await this.updateAccount(accountId, { + rateLimitedAt: now.toISOString(), + rateLimitStatus: 'limited', + rateLimitResetAt: resetAt.toISOString(), + rateLimitDuration: rateLimitDuration.toString(), + status: 'rateLimited', + schedulable: 'false', // 防止被调度 + errorMessage: `Rate limited until ${resetAt.toISOString()}` + }) + + logger.warn( + `⏳ Gemini-API account ${account.name} marked as rate limited for ${rateLimitDuration} minutes (until ${resetAt.toISOString()})` + ) + } else { + // 清除限流状态 + await this.updateAccount(accountId, { + rateLimitedAt: '', + rateLimitStatus: '', + rateLimitResetAt: '', + status: 'active', + schedulable: 'true', + errorMessage: '' + }) + + logger.info(`✅ Rate limit cleared for Gemini-API account ${account.name}`) + } + } + + // 🚫 标记账户为未授权状态(401错误) + async markAccountUnauthorized(accountId, reason = 'Gemini API账号认证失败(401错误)') { + const account = await this.getAccount(accountId) + if (!account) { + return + } + + const now = new Date().toISOString() + const currentCount = parseInt(account.unauthorizedCount || '0', 10) + const unauthorizedCount = Number.isFinite(currentCount) ? currentCount + 1 : 1 + + await this.updateAccount(accountId, { + status: 'unauthorized', + schedulable: 'false', + errorMessage: reason, + unauthorizedAt: now, + unauthorizedCount: unauthorizedCount.toString() + }) + + logger.warn( + `🚫 Gemini-API account ${account.name || accountId} marked as unauthorized due to 401 error` + ) + + try { + const webhookNotifier = require('../utils/webhookNotifier') + await webhookNotifier.sendAccountAnomalyNotification({ + accountId, + accountName: account.name || accountId, + platform: 'gemini-api', + status: 'unauthorized', + errorCode: 'GEMINI_API_UNAUTHORIZED', + reason, + timestamp: now + }) + logger.info( + `📢 Webhook notification sent for Gemini-API account ${account.name || accountId} unauthorized state` + ) + } catch (webhookError) { + logger.error('Failed to send unauthorized webhook notification:', webhookError) + } + } + + // 检查并清除过期的限流状态 + async checkAndClearRateLimit(accountId) { + const account = await this.getAccount(accountId) + if (!account || account.rateLimitStatus !== 'limited') { + return false + } + + const now = new Date() + let shouldClear = false + + // 优先使用 rateLimitResetAt 字段 + if (account.rateLimitResetAt) { + const resetAt = new Date(account.rateLimitResetAt) + shouldClear = now >= resetAt + } else { + // 如果没有 rateLimitResetAt,使用旧的逻辑 + const rateLimitedAt = new Date(account.rateLimitedAt) + const rateLimitDuration = parseInt(account.rateLimitDuration) || 60 + shouldClear = now - rateLimitedAt > rateLimitDuration * 60000 + } + + if (shouldClear) { + // 限流已过期,清除状态 + await this.setAccountRateLimited(accountId, false) + return true + } + + return false + } + + // 切换调度状态 + async toggleSchedulable(accountId) { + const account = await this.getAccount(accountId) + if (!account) { + throw new Error('Account not found') + } + + const newSchedulableStatus = account.schedulable === 'true' ? 'false' : 'true' + await this.updateAccount(accountId, { + schedulable: newSchedulableStatus + }) + + logger.info( + `🔄 Toggled schedulable status for Gemini-API account ${account.name}: ${newSchedulableStatus}` + ) + + return { + success: true, + schedulable: newSchedulableStatus === 'true' + } + } + + // 重置账户状态(清除所有异常状态) + async resetAccountStatus(accountId) { + const account = await this.getAccount(accountId) + if (!account) { + throw new Error('Account not found') + } + + const updates = { + // 根据是否有有效的 apiKey 来设置 status + status: account.apiKey ? 'active' : 'created', + // 恢复可调度状态 + schedulable: 'true', + // 清除错误相关字段 + errorMessage: '', + rateLimitedAt: '', + rateLimitStatus: '', + rateLimitResetAt: '', + rateLimitDuration: '' + } + + await this.updateAccount(accountId, updates) + logger.info(`✅ Reset all error status for Gemini-API account ${accountId}`) + + // 发送 Webhook 通知 + try { + const webhookNotifier = require('../utils/webhookNotifier') + await webhookNotifier.sendAccountAnomalyNotification({ + accountId, + accountName: account.name || accountId, + platform: 'gemini-api', + status: 'recovered', + errorCode: 'STATUS_RESET', + reason: 'Account status manually reset', + timestamp: new Date().toISOString() + }) + logger.info( + `📢 Webhook notification sent for Gemini-API account ${account.name} status reset` + ) + } catch (webhookError) { + logger.error('Failed to send status reset webhook notification:', webhookError) + } + + return { success: true, message: 'Account status reset successfully' } + } + + // API Key 不会过期 + isTokenExpired(_account) { + return false + } + + // 获取限流信息 + _getRateLimitInfo(accountData) { + if (accountData.rateLimitStatus !== 'limited') { + return { isRateLimited: false } + } + + const now = new Date() + let willBeAvailableAt + let remainingMinutes + + // 优先使用 rateLimitResetAt 字段 + if (accountData.rateLimitResetAt) { + willBeAvailableAt = new Date(accountData.rateLimitResetAt) + remainingMinutes = Math.max(0, Math.ceil((willBeAvailableAt - now) / 60000)) + } else { + // 如果没有 rateLimitResetAt,使用旧的逻辑 + const rateLimitedAt = new Date(accountData.rateLimitedAt) + const rateLimitDuration = parseInt(accountData.rateLimitDuration) || 60 + const elapsedMinutes = Math.floor((now - rateLimitedAt) / 60000) + remainingMinutes = Math.max(0, rateLimitDuration - elapsedMinutes) + willBeAvailableAt = new Date(rateLimitedAt.getTime() + rateLimitDuration * 60000) + } + + return { + isRateLimited: remainingMinutes > 0, + remainingMinutes, + willBeAvailableAt + } + } + + // 加密敏感数据 + _encryptSensitiveData(text) { + if (!text) { + return '' + } + + const key = this._getEncryptionKey() + const iv = crypto.randomBytes(16) + const cipher = crypto.createCipheriv(this.ENCRYPTION_ALGORITHM, key, iv) + + let encrypted = cipher.update(text) + encrypted = Buffer.concat([encrypted, cipher.final()]) + + return `${iv.toString('hex')}:${encrypted.toString('hex')}` + } + + // 解密敏感数据 + _decryptSensitiveData(text) { + if (!text || text === '') { + return '' + } + + // 检查缓存 + const cacheKey = crypto.createHash('sha256').update(text).digest('hex') + const cached = this._decryptCache.get(cacheKey) + if (cached !== undefined) { + return cached + } + + try { + const key = this._getEncryptionKey() + const [ivHex, encryptedHex] = text.split(':') + + const iv = Buffer.from(ivHex, 'hex') + const encryptedText = Buffer.from(encryptedHex, 'hex') + + const decipher = crypto.createDecipheriv(this.ENCRYPTION_ALGORITHM, key, iv) + let decrypted = decipher.update(encryptedText) + decrypted = Buffer.concat([decrypted, decipher.final()]) + + const result = decrypted.toString() + + // 存入缓存(5分钟过期) + this._decryptCache.set(cacheKey, result, 5 * 60 * 1000) + + return result + } catch (error) { + logger.error('Decryption error:', error) + return '' + } + } + + // 获取加密密钥 + _getEncryptionKey() { + if (!this._encryptionKeyCache) { + this._encryptionKeyCache = crypto.scryptSync( + config.security.encryptionKey, + this.ENCRYPTION_SALT, + 32 + ) + } + return this._encryptionKeyCache + } + + // 保存账户到 Redis + async _saveAccount(accountId, accountData) { + const client = redis.getClientSafe() + const key = `${this.ACCOUNT_KEY_PREFIX}${accountId}` + + // 保存账户数据 + await client.hset(key, accountData) + + // 添加到共享账户列表 + if (accountData.accountType === 'shared') { + await client.sadd(this.SHARED_ACCOUNTS_KEY, accountId) + } + } +} + +module.exports = new GeminiApiAccountService() diff --git a/src/services/unifiedGeminiScheduler.js b/src/services/unifiedGeminiScheduler.js index 88f793bd..1b71d344 100644 --- a/src/services/unifiedGeminiScheduler.js +++ b/src/services/unifiedGeminiScheduler.js @@ -1,4 +1,5 @@ const geminiAccountService = require('./geminiAccountService') +const geminiApiAccountService = require('./geminiApiAccountService') const accountGroupService = require('./accountGroupService') const redis = require('../models/redis') const logger = require('../utils/logger') @@ -19,35 +20,69 @@ class UnifiedGeminiScheduler { } // 🎯 统一调度Gemini账号 - async selectAccountForApiKey(apiKeyData, sessionHash = null, requestedModel = null) { + async selectAccountForApiKey( + apiKeyData, + sessionHash = null, + requestedModel = null, + options = {} + ) { + const { allowApiAccounts = false } = options + try { // 如果API Key绑定了专属账户或分组,优先使用 if (apiKeyData.geminiAccountId) { + // 检查是否是 Gemini API 账户(api: 前缀) + if (apiKeyData.geminiAccountId.startsWith('api:')) { + const accountId = apiKeyData.geminiAccountId.replace('api:', '') + const boundAccount = await geminiApiAccountService.getAccount(accountId) + if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') { + logger.info( + `🎯 Using bound Gemini-API account: ${boundAccount.name} (${accountId}) for API key ${apiKeyData.name}` + ) + // 更新账户的最后使用时间 + await geminiApiAccountService.markAccountUsed(accountId) + return { + accountId, + accountType: 'gemini-api' + } + } else { + // 提供详细的不可用原因 + const reason = !boundAccount + ? 'account not found' + : boundAccount.isActive !== 'true' + ? `isActive=${boundAccount.isActive}` + : `status=${boundAccount.status}` + logger.warn( + `⚠️ Bound Gemini-API account ${accountId} is not available (${reason}), falling back to pool` + ) + } + } // 检查是否是分组 - if (apiKeyData.geminiAccountId.startsWith('group:')) { + else if (apiKeyData.geminiAccountId.startsWith('group:')) { const groupId = apiKeyData.geminiAccountId.replace('group:', '') logger.info( `🎯 API key ${apiKeyData.name} is bound to group ${groupId}, selecting from group` ) return await this.selectAccountFromGroup(groupId, sessionHash, requestedModel, apiKeyData) } - - // 普通专属账户 - const boundAccount = await geminiAccountService.getAccount(apiKeyData.geminiAccountId) - if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') { - logger.info( - `🎯 Using bound dedicated Gemini account: ${boundAccount.name} (${apiKeyData.geminiAccountId}) for API key ${apiKeyData.name}` - ) - // 更新账户的最后使用时间 - await geminiAccountService.markAccountUsed(apiKeyData.geminiAccountId) - return { - accountId: apiKeyData.geminiAccountId, - accountType: 'gemini' + // 普通 Gemini OAuth 专属账户 + else { + const boundAccount = await geminiAccountService.getAccount(apiKeyData.geminiAccountId) + if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') { + logger.info( + `🎯 Using bound dedicated Gemini account: ${boundAccount.name} (${apiKeyData.geminiAccountId}) for API key ${apiKeyData.name}` + ) + // 更新账户的最后使用时间 + await geminiAccountService.markAccountUsed(apiKeyData.geminiAccountId) + return { + accountId: apiKeyData.geminiAccountId, + accountType: 'gemini' + } + } else { + logger.warn( + `⚠️ Bound Gemini account ${apiKeyData.geminiAccountId} is not available, falling back to pool` + ) } - } else { - logger.warn( - `⚠️ Bound Gemini account ${apiKeyData.geminiAccountId} is not available, falling back to pool` - ) } } @@ -66,8 +101,12 @@ class UnifiedGeminiScheduler { logger.info( `🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}` ) - // 更新账户的最后使用时间 - await geminiAccountService.markAccountUsed(mappedAccount.accountId) + // 更新账户的最后使用时间(根据账户类型调用正确的服务) + if (mappedAccount.accountType === 'gemini-api') { + await geminiApiAccountService.markAccountUsed(mappedAccount.accountId) + } else { + await geminiAccountService.markAccountUsed(mappedAccount.accountId) + } return mappedAccount } else { logger.warn( @@ -79,7 +118,11 @@ class UnifiedGeminiScheduler { } // 获取所有可用账户 - const availableAccounts = await this._getAllAvailableAccounts(apiKeyData, requestedModel) + const availableAccounts = await this._getAllAvailableAccounts( + apiKeyData, + requestedModel, + allowApiAccounts + ) if (availableAccounts.length === 0) { // 提供更详细的错误信息 @@ -114,8 +157,12 @@ class UnifiedGeminiScheduler { `🎯 Selected account: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) with priority ${selectedAccount.priority} for API key ${apiKeyData.name}` ) - // 更新账户的最后使用时间 - await geminiAccountService.markAccountUsed(selectedAccount.accountId) + // 更新账户的最后使用时间(根据账户类型调用正确的服务) + if (selectedAccount.accountType === 'gemini-api') { + await geminiApiAccountService.markAccountUsed(selectedAccount.accountId) + } else { + await geminiAccountService.markAccountUsed(selectedAccount.accountId) + } return { accountId: selectedAccount.accountId, @@ -128,53 +175,104 @@ class UnifiedGeminiScheduler { } // 📋 获取所有可用账户 - async _getAllAvailableAccounts(apiKeyData, requestedModel = null) { + async _getAllAvailableAccounts(apiKeyData, requestedModel = null, allowApiAccounts = false) { const availableAccounts = [] // 如果API Key绑定了专属账户,优先返回 if (apiKeyData.geminiAccountId) { - const boundAccount = await geminiAccountService.getAccount(apiKeyData.geminiAccountId) - if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') { - const isRateLimited = await this.isAccountRateLimited(boundAccount.id) - if (!isRateLimited) { - // 检查模型支持 - if ( - requestedModel && - boundAccount.supportedModels && - boundAccount.supportedModels.length > 0 - ) { - // 处理可能带有 models/ 前缀的模型名 - const normalizedModel = requestedModel.replace('models/', '') - const modelSupported = boundAccount.supportedModels.some( - (model) => model.replace('models/', '') === normalizedModel - ) - if (!modelSupported) { - logger.warn( - `⚠️ Bound Gemini account ${boundAccount.name} does not support model ${requestedModel}` + // 检查是否是 Gemini API 账户(api: 前缀) + if (apiKeyData.geminiAccountId.startsWith('api:')) { + const accountId = apiKeyData.geminiAccountId.replace('api:', '') + const boundAccount = await geminiApiAccountService.getAccount(accountId) + if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') { + const isRateLimited = await this.isAccountRateLimited(accountId) + if (!isRateLimited) { + // 检查模型支持 + if ( + requestedModel && + boundAccount.supportedModels && + boundAccount.supportedModels.length > 0 + ) { + const normalizedModel = requestedModel.replace('models/', '') + const modelSupported = boundAccount.supportedModels.some( + (model) => model.replace('models/', '') === normalizedModel ) - return availableAccounts + if (!modelSupported) { + logger.warn( + `⚠️ Bound Gemini-API account ${boundAccount.name} does not support model ${requestedModel}` + ) + return availableAccounts + } } - } - logger.info( - `🎯 Using bound dedicated Gemini account: ${boundAccount.name} (${apiKeyData.geminiAccountId})` + logger.info(`🎯 Using bound Gemini-API account: ${boundAccount.name} (${accountId})`) + return [ + { + ...boundAccount, + accountId, + accountType: 'gemini-api', + priority: parseInt(boundAccount.priority) || 50, + lastUsedAt: boundAccount.lastUsedAt || '0' + } + ] + } + } else { + // 提供详细的不可用原因 + const reason = !boundAccount + ? 'account not found' + : boundAccount.isActive !== 'true' + ? `isActive=${boundAccount.isActive}` + : `status=${boundAccount.status}` + logger.warn( + `⚠️ Bound Gemini-API account ${accountId} is not available in _getAllAvailableAccounts (${reason})` ) - return [ - { - ...boundAccount, - accountId: boundAccount.id, - accountType: 'gemini', - priority: parseInt(boundAccount.priority) || 50, - lastUsedAt: boundAccount.lastUsedAt || '0' - } - ] } - } else { - logger.warn(`⚠️ Bound Gemini account ${apiKeyData.geminiAccountId} is not available`) + } + // 普通 Gemini OAuth 账户 + else if (!apiKeyData.geminiAccountId.startsWith('group:')) { + const boundAccount = await geminiAccountService.getAccount(apiKeyData.geminiAccountId) + if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') { + const isRateLimited = await this.isAccountRateLimited(boundAccount.id) + if (!isRateLimited) { + // 检查模型支持 + if ( + requestedModel && + boundAccount.supportedModels && + boundAccount.supportedModels.length > 0 + ) { + // 处理可能带有 models/ 前缀的模型名 + const normalizedModel = requestedModel.replace('models/', '') + const modelSupported = boundAccount.supportedModels.some( + (model) => model.replace('models/', '') === normalizedModel + ) + if (!modelSupported) { + logger.warn( + `⚠️ Bound Gemini account ${boundAccount.name} does not support model ${requestedModel}` + ) + return availableAccounts + } + } + + logger.info( + `🎯 Using bound dedicated Gemini account: ${boundAccount.name} (${apiKeyData.geminiAccountId})` + ) + return [ + { + ...boundAccount, + accountId: boundAccount.id, + accountType: 'gemini', + priority: parseInt(boundAccount.priority) || 50, + lastUsedAt: boundAccount.lastUsedAt || '0' + } + ] + } + } else { + logger.warn(`⚠️ Bound Gemini account ${apiKeyData.geminiAccountId} is not available`) + } } } - // 获取所有Gemini账户(共享池) + // 获取所有Gemini OAuth账户(共享池) const geminiAccounts = await geminiAccountService.getAllAccounts() for (const account of geminiAccounts) { if ( @@ -223,7 +321,48 @@ class UnifiedGeminiScheduler { } } - logger.info(`📊 Total available Gemini accounts: ${availableAccounts.length}`) + // 如果允许调度 Gemini API 账户,则添加到可用列表 + if (allowApiAccounts) { + const geminiApiAccounts = await geminiApiAccountService.getAllAccounts() + for (const account of geminiApiAccounts) { + if ( + account.isActive === 'true' && + account.status !== 'error' && + (account.accountType === 'shared' || !account.accountType) && + this._isSchedulable(account.schedulable) + ) { + // 检查模型支持 + if (requestedModel && account.supportedModels && account.supportedModels.length > 0) { + const normalizedModel = requestedModel.replace('models/', '') + const modelSupported = account.supportedModels.some( + (model) => model.replace('models/', '') === normalizedModel + ) + if (!modelSupported) { + logger.debug( + `⏭️ Skipping Gemini-API account ${account.name} - doesn't support model ${requestedModel}` + ) + continue + } + } + + // 检查是否被限流 + const isRateLimited = await this.isAccountRateLimited(account.id) + if (!isRateLimited) { + availableAccounts.push({ + ...account, + accountId: account.id, + accountType: 'gemini-api', + priority: parseInt(account.priority) || 50, + lastUsedAt: account.lastUsedAt || '0' + }) + } + } + } + } + + logger.info( + `📊 Total available accounts: ${availableAccounts.length} (Gemini OAuth + ${allowApiAccounts ? 'Gemini API' : 'no API accounts'})` + ) return availableAccounts } @@ -256,6 +395,17 @@ class UnifiedGeminiScheduler { return false } return !(await this.isAccountRateLimited(accountId)) + } else if (accountType === 'gemini-api') { + const account = await geminiApiAccountService.getAccount(accountId) + if (!account || account.isActive !== 'true' || account.status === 'error') { + return false + } + // 检查是否可调度 + if (!this._isSchedulable(account.schedulable)) { + logger.info(`🚫 Gemini-API account ${accountId} is not schedulable`) + return false + } + return !(await this.isAccountRateLimited(accountId)) } return false } catch (error) { @@ -344,6 +494,8 @@ class UnifiedGeminiScheduler { try { if (accountType === 'gemini') { await geminiAccountService.setAccountRateLimited(accountId, true) + } else if (accountType === 'gemini-api') { + await geminiApiAccountService.setAccountRateLimited(accountId, true) } // 删除会话映射 @@ -366,6 +518,8 @@ class UnifiedGeminiScheduler { try { if (accountType === 'gemini') { await geminiAccountService.setAccountRateLimited(accountId, false) + } else if (accountType === 'gemini-api') { + await geminiApiAccountService.setAccountRateLimited(accountId, false) } return { success: true } @@ -379,9 +533,23 @@ class UnifiedGeminiScheduler { } // 🔍 检查账户是否处于限流状态 - async isAccountRateLimited(accountId) { + async isAccountRateLimited(accountId, accountType = null) { try { - const account = await geminiAccountService.getAccount(accountId) + let account = null + + // 如果指定了账户类型,直接使用对应服务 + if (accountType === 'gemini-api') { + account = await geminiApiAccountService.getAccount(accountId) + } else if (accountType === 'gemini') { + account = await geminiAccountService.getAccount(accountId) + } else { + // 未指定类型,先尝试 gemini,再尝试 gemini-api + account = await geminiAccountService.getAccount(accountId) + if (!account) { + account = await geminiApiAccountService.getAccount(accountId) + } + } + if (!account) { return false } @@ -389,7 +557,9 @@ class UnifiedGeminiScheduler { if (account.rateLimitStatus === 'limited' && account.rateLimitedAt) { const limitedAt = new Date(account.rateLimitedAt).getTime() const now = Date.now() - const limitDuration = 60 * 60 * 1000 // 1小时 + // 使用账户配置的限流时长,默认1小时 + const rateLimitDuration = parseInt(account.rateLimitDuration) || 60 + const limitDuration = rateLimitDuration * 60 * 1000 return now < limitedAt + limitDuration } @@ -400,7 +570,7 @@ class UnifiedGeminiScheduler { } } - // 👥 从分组中选择账户 + // 👥 从分组中选择账户(支持 Gemini OAuth 和 Gemini API 两种账户类型) async selectAccountFromGroup(groupId, sessionHash = null, requestedModel = null) { try { // 获取分组信息 @@ -432,8 +602,12 @@ class UnifiedGeminiScheduler { logger.info( `🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}` ) - // 更新账户的最后使用时间 - await geminiAccountService.markAccountUsed(mappedAccount.accountId) + // 更新账户的最后使用时间(根据账户类型调用正确的服务) + if (mappedAccount.accountType === 'gemini-api') { + await geminiApiAccountService.markAccountUsed(mappedAccount.accountId) + } else { + await geminiAccountService.markAccountUsed(mappedAccount.accountId) + } return mappedAccount } } @@ -450,9 +624,17 @@ class UnifiedGeminiScheduler { const availableAccounts = [] - // 获取所有成员账户的详细信息 + // 获取所有成员账户的详细信息(支持 Gemini OAuth 和 Gemini API 两种类型) for (const memberId of memberIds) { - const account = await geminiAccountService.getAccount(memberId) + // 首先尝试从 Gemini OAuth 账户服务获取 + let account = await geminiAccountService.getAccount(memberId) + let accountType = 'gemini' + + // 如果 Gemini OAuth 账户不存在,尝试从 Gemini API 账户服务获取 + if (!account) { + account = await geminiApiAccountService.getAccount(memberId) + accountType = 'gemini-api' + } if (!account) { logger.warn(`⚠️ Gemini account ${memberId} not found in group ${group.name}`) @@ -465,13 +647,15 @@ class UnifiedGeminiScheduler { account.status !== 'error' && this._isSchedulable(account.schedulable) ) { - // 检查token是否过期 - const isExpired = geminiAccountService.isTokenExpired(account) - if (isExpired && !account.refreshToken) { - logger.warn( - `⚠️ Gemini account ${account.name} in group token expired and no refresh token available` - ) - continue + // 对于 Gemini OAuth 账户,检查 token 是否过期 + if (accountType === 'gemini') { + const isExpired = geminiAccountService.isTokenExpired(account) + if (isExpired && !account.refreshToken) { + logger.warn( + `⚠️ Gemini account ${account.name} in group token expired and no refresh token available` + ) + continue + } } // 检查模型支持 @@ -483,19 +667,19 @@ class UnifiedGeminiScheduler { ) if (!modelSupported) { logger.debug( - `⏭️ Skipping Gemini account ${account.name} in group - doesn't support model ${requestedModel}` + `⏭️ Skipping ${accountType} account ${account.name} in group - doesn't support model ${requestedModel}` ) continue } } // 检查是否被限流 - const isRateLimited = await this.isAccountRateLimited(account.id) + const isRateLimited = await this.isAccountRateLimited(account.id, accountType) if (!isRateLimited) { availableAccounts.push({ ...account, accountId: account.id, - accountType: 'gemini', + accountType, priority: parseInt(account.priority) || 50, lastUsedAt: account.lastUsedAt || '0' }) @@ -529,8 +713,12 @@ class UnifiedGeminiScheduler { `🎯 Selected account from Gemini group ${group.name}: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) with priority ${selectedAccount.priority}` ) - // 更新账户的最后使用时间 - await geminiAccountService.markAccountUsed(selectedAccount.accountId) + // 更新账户的最后使用时间(根据账户类型调用正确的服务) + if (selectedAccount.accountType === 'gemini-api') { + await geminiApiAccountService.markAccountUsed(selectedAccount.accountId) + } else { + await geminiAccountService.markAccountUsed(selectedAccount.accountId) + } return { accountId: selectedAccount.accountId, diff --git a/web/admin-spa/src/components/accounts/AccountForm.vue b/web/admin-spa/src/components/accounts/AccountForm.vue index b9744b89..0c696f32 100644 --- a/web/admin-spa/src/components/accounts/AccountForm.vue +++ b/web/admin-spa/src/components/accounts/AccountForm.vue @@ -477,6 +477,37 @@ + + @@ -519,7 +550,8 @@ form.platform !== 'ccr' && form.platform !== 'bedrock' && form.platform !== 'azure_openai' && - form.platform !== 'openai-responses' + form.platform !== 'openai-responses' && + form.platform !== 'gemini-api' " >