From 6f6c27487711e42effb44d99c37e3c72439d3f1f Mon Sep 17 00:00:00 2001 From: shaw Date: Sun, 12 Oct 2025 22:13:38 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=A7=E7=BB=AD=E4=BF=AE=E5=A4=8DPR-5?= =?UTF-8?q?41=E9=81=97=E7=95=99=E7=9A=84=E7=B3=BB=E5=88=97bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/admin.js | 105 ++++++++++++++---- src/services/azureOpenaiAccountService.js | 42 ++++++- src/services/bedrockAccountService.js | 51 +++++++-- src/services/ccrAccountService.js | 49 ++++++-- src/services/claudeAccountService.js | 10 +- src/services/claudeConsoleAccountService.js | 49 ++++++-- src/services/geminiAccountService.js | 39 ++++++- src/services/openaiAccountService.js | 46 +++++++- src/services/openaiResponsesAccountService.js | 38 ++++++- 9 files changed, 368 insertions(+), 61 deletions(-) diff --git a/src/routes/admin.js b/src/routes/admin.js index bf61bd26..81463a2a 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -2267,7 +2267,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => { useUnifiedUserAgent, useUnifiedClientId, unifiedClientId, - expiresAt + expiresAt, + subscriptionExpiresAt } = req.body if (!name) { @@ -2311,7 +2312,7 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => { useUnifiedUserAgent: useUnifiedUserAgent === true, // 默认为false useUnifiedClientId: useUnifiedClientId === true, // 默认为false unifiedClientId: unifiedClientId || '', // 统一的客户端标识 - expiresAt: expiresAt || null // 账户订阅到期时间 + expiresAt: subscriptionExpiresAt ?? expiresAt ?? null // 账户订阅到期时间 }) // 如果是分组类型,将账户添加到分组 @@ -2400,8 +2401,12 @@ router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) => // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt const mappedUpdates = { ...updates } - if ('expiresAt' in mappedUpdates) { + if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) { + mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt + } else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) { mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt + } + if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) { delete mappedUpdates.expiresAt } @@ -2797,8 +2802,12 @@ router.put('/claude-console-accounts/:accountId', authenticateAdmin, async (req, // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt const mappedUpdates = { ...updates } - if ('expiresAt' in mappedUpdates) { + if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) { + mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt + } else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) { mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt + } + if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) { delete mappedUpdates.expiresAt } @@ -3214,8 +3223,12 @@ router.put('/ccr-accounts/:accountId', authenticateAdmin, async (req, res) => { // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt const mappedUpdates = { ...updates } - if ('expiresAt' in mappedUpdates) { + if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) { + mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt + } else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) { mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt + } + if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) { delete mappedUpdates.expiresAt } @@ -3582,8 +3595,12 @@ router.put('/bedrock-accounts/:accountId', authenticateAdmin, async (req, res) = // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt const mappedUpdates = { ...updates } - if ('expiresAt' in mappedUpdates) { + if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) { + mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt + } else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) { mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt + } + if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) { delete mappedUpdates.expiresAt } @@ -3912,8 +3929,11 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => { return { ...account, - // 映射字段:使用 subscriptionExpiresAt 作为前端显示的 expiresAt - expiresAt: account.subscriptionExpiresAt || null, + expiresAt: account.expiresAt || null, + subscriptionExpiresAt: + account.subscriptionExpiresAt && account.subscriptionExpiresAt !== '' + ? account.subscriptionExpiresAt + : null, groupInfos, usage: { daily: usageStats.daily, @@ -3931,8 +3951,11 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => { const groupInfos = await accountGroupService.getAccountGroups(account.id) return { ...account, - // 映射字段:使用 subscriptionExpiresAt 作为前端显示的 expiresAt - expiresAt: account.subscriptionExpiresAt || null, + expiresAt: account.expiresAt || null, + subscriptionExpiresAt: + account.subscriptionExpiresAt && account.subscriptionExpiresAt !== '' + ? account.subscriptionExpiresAt + : null, groupInfos, usage: { daily: { tokens: 0, requests: 0, allTokens: 0 }, @@ -3947,6 +3970,11 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => { ) return { ...account, + expiresAt: account.expiresAt || null, + subscriptionExpiresAt: + account.subscriptionExpiresAt && account.subscriptionExpiresAt !== '' + ? account.subscriptionExpiresAt + : null, groupInfos: [], usage: { daily: { tokens: 0, requests: 0, allTokens: 0 }, @@ -4059,8 +4087,12 @@ router.put('/gemini-accounts/:accountId', authenticateAdmin, async (req, res) => // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt const mappedUpdates = { ...updates } - if ('expiresAt' in mappedUpdates) { + if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) { + mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt + } else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) { mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt + } + if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) { delete mappedUpdates.expiresAt } @@ -7270,7 +7302,8 @@ router.post('/openai-accounts', authenticateAdmin, async (req, res) => { rateLimitDuration, priority, needsImmediateRefresh, // 是否需要立即刷新 - requireRefreshSuccess // 是否必须刷新成功才能创建 + requireRefreshSuccess, // 是否必须刷新成功才能创建 + subscriptionExpiresAt } = req.body if (!name) { @@ -7292,7 +7325,8 @@ router.post('/openai-accounts', authenticateAdmin, async (req, res) => { accountInfo: accountInfo || {}, proxy: proxy || null, isActive: true, - schedulable: true + schedulable: true, + subscriptionExpiresAt: subscriptionExpiresAt || null } // 如果需要立即刷新且必须成功(OpenAI 手动模式) @@ -7571,10 +7605,16 @@ router.put('/openai-accounts/:id', authenticateAdmin, async (req, res) => { : currentAccount.emailVerified } - // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt (订阅过期时间) - // 注意:这里不影响上面 OAuth token 的 expiresAt 字段 - if ('expiresAt' in updates && !updates.openaiOauth?.expires_in) { + const hasOauthExpiry = Boolean(updates.openaiOauth?.expires_in) + + // 处理订阅过期时间字段:优先使用 subscriptionExpiresAt,兼容旧版的 expiresAt + if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) { + updateData.subscriptionExpiresAt = updates.subscriptionExpiresAt + } else if (Object.prototype.hasOwnProperty.call(updates, 'expiresAt') && !hasOauthExpiry) { updateData.subscriptionExpiresAt = updates.expiresAt + } + + if (!hasOauthExpiry && Object.prototype.hasOwnProperty.call(updateData, 'subscriptionExpiresAt')) { delete updateData.expiresAt } @@ -7966,8 +8006,12 @@ router.put('/azure-openai-accounts/:id', authenticateAdmin, async (req, res) => // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt const mappedUpdates = { ...updates } - if ('expiresAt' in mappedUpdates) { + if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) { + mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt + } else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) { mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt + } + if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) { delete mappedUpdates.expiresAt } @@ -8346,8 +8390,12 @@ router.put('/openai-responses-accounts/:id', authenticateAdmin, async (req, res) // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt const mappedUpdates = { ...updates } - if ('expiresAt' in mappedUpdates) { + if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) { + mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt + } else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) { mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt + } + if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) { delete mappedUpdates.expiresAt } @@ -8717,9 +8765,11 @@ router.get('/droid-accounts', authenticateAdmin, async (req, res) => { return { ...account, - // 映射字段:使用 subscriptionExpiresAt 作为前端显示的 expiresAt - // OAuth token 的原始 expiresAt 保留在内部使用 - expiresAt: account.subscriptionExpiresAt || null, + expiresAt: account.expiresAt || null, + subscriptionExpiresAt: + account.subscriptionExpiresAt && account.subscriptionExpiresAt !== '' + ? account.subscriptionExpiresAt + : null, schedulable: account.schedulable === 'true', boundApiKeysCount, groupInfos, @@ -8733,8 +8783,11 @@ router.get('/droid-accounts', authenticateAdmin, async (req, res) => { logger.warn(`Failed to get stats for Droid account ${account.id}:`, error.message) return { ...account, - // 映射字段:使用 subscriptionExpiresAt 作为前端显示的 expiresAt - expiresAt: account.subscriptionExpiresAt || null, + expiresAt: account.expiresAt || null, + subscriptionExpiresAt: + account.subscriptionExpiresAt && account.subscriptionExpiresAt !== '' + ? account.subscriptionExpiresAt + : null, boundApiKeysCount: 0, groupInfos: [], usage: { @@ -8851,8 +8904,12 @@ router.put('/droid-accounts/:id', authenticateAdmin, async (req, res) => { // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt const mappedUpdates = { ...updates } - if ('expiresAt' in mappedUpdates) { + if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) { + mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt + } else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) { mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt + } + if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) { delete mappedUpdates.expiresAt } diff --git a/src/services/azureOpenaiAccountService.js b/src/services/azureOpenaiAccountService.js index e7d5754d..09419f65 100644 --- a/src/services/azureOpenaiAccountService.js +++ b/src/services/azureOpenaiAccountService.js @@ -65,6 +65,19 @@ const AZURE_OPENAI_ACCOUNT_KEY_PREFIX = 'azure_openai:account:' const SHARED_AZURE_OPENAI_ACCOUNTS_KEY = 'shared_azure_openai_accounts' const ACCOUNT_SESSION_MAPPING_PREFIX = 'azure_openai_session_account_mapping:' +function normalizeSubscriptionExpiresAt(value) { + if (value === undefined || value === null || value === '') { + return '' + } + + const date = value instanceof Date ? value : new Date(value) + if (Number.isNaN(date.getTime())) { + return '' + } + + return date.toISOString() +} + // 加密函数 function encrypt(text) { if (!text) { @@ -133,6 +146,9 @@ async function createAccount(accountData) { isActive: accountData.isActive !== false ? 'true' : 'false', status: 'active', schedulable: accountData.schedulable !== false ? 'true' : 'false', + subscriptionExpiresAt: normalizeSubscriptionExpiresAt( + accountData.subscriptionExpiresAt || '' + ), createdAt: now, updatedAt: now } @@ -152,7 +168,10 @@ async function createAccount(accountData) { } logger.info(`Created Azure OpenAI account: ${accountId}`) - return account + return { + ...account, + subscriptionExpiresAt: account.subscriptionExpiresAt || null + } } // 获取账户 @@ -187,6 +206,11 @@ async function getAccount(accountId) { } } + accountData.subscriptionExpiresAt = + accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== '' + ? accountData.subscriptionExpiresAt + : null + return accountData } @@ -218,6 +242,15 @@ async function updateAccount(accountId, updates) { : JSON.stringify(updates.supportedModels) } + if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) { + updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt( + updates.subscriptionExpiresAt + ) + } else if (Object.prototype.hasOwnProperty.call(updates, 'expiresAt')) { + updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.expiresAt) + delete updates.expiresAt + } + // 更新账户类型时处理共享账户集合 const client = redisClient.getClientSafe() if (updates.accountType && updates.accountType !== existingAccount.accountType) { @@ -244,6 +277,10 @@ async function updateAccount(accountId, updates) { } } + if (!updatedAccount.subscriptionExpiresAt) { + updatedAccount.subscriptionExpiresAt = null + } + return updatedAccount } @@ -303,7 +340,8 @@ async function getAllAccounts() { accounts.push({ ...accountData, isActive: accountData.isActive === 'true', - schedulable: accountData.schedulable !== 'false' + schedulable: accountData.schedulable !== 'false', + subscriptionExpiresAt: accountData.subscriptionExpiresAt || null }) } } diff --git a/src/services/bedrockAccountService.js b/src/services/bedrockAccountService.js index b5e9e1a9..66798a6c 100644 --- a/src/services/bedrockAccountService.js +++ b/src/services/bedrockAccountService.js @@ -6,6 +6,19 @@ const config = require('../../config/config') const bedrockRelayService = require('./bedrockRelayService') const LRUCache = require('../utils/lruCache') +function normalizeSubscriptionExpiresAt(value) { + if (value === undefined || value === null || value === '') { + return '' + } + + const date = value instanceof Date ? value : new Date(value) + if (Number.isNaN(date.getTime())) { + return '' + } + + return date.toISOString() +} + class BedrockAccountService { constructor() { // 加密相关常量 @@ -40,7 +53,8 @@ class BedrockAccountService { accountType = 'shared', // 'dedicated' or 'shared' priority = 50, // 调度优先级 (1-100,数字越小优先级越高) schedulable = true, // 是否可被调度 - credentialType = 'default' // 'default', 'access_key', 'bearer_token' + credentialType = 'default', // 'default', 'access_key', 'bearer_token' + subscriptionExpiresAt = null } = options const accountId = uuidv4() @@ -56,6 +70,7 @@ class BedrockAccountService { priority, schedulable, credentialType, + subscriptionExpiresAt: normalizeSubscriptionExpiresAt(subscriptionExpiresAt), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), type: 'bedrock' // 标识这是Bedrock账户 @@ -84,6 +99,7 @@ class BedrockAccountService { priority, schedulable, credentialType, + subscriptionExpiresAt: accountData.subscriptionExpiresAt || null, createdAt: accountData.createdAt, type: 'bedrock' } @@ -106,6 +122,11 @@ class BedrockAccountService { account.awsCredentials = this._decryptAwsCredentials(account.awsCredentials) } + account.subscriptionExpiresAt = + account.subscriptionExpiresAt && account.subscriptionExpiresAt !== '' + ? account.subscriptionExpiresAt + : null + logger.debug(`🔍 获取Bedrock账户 - ID: ${accountId}, 名称: ${account.name}`) return { @@ -141,13 +162,15 @@ class BedrockAccountService { accountType: account.accountType, priority: account.priority, schedulable: account.schedulable, - credentialType: account.credentialType, - createdAt: account.createdAt, - updatedAt: account.updatedAt, - type: 'bedrock', - hasCredentials: !!account.awsCredentials - }) - } + credentialType: account.credentialType, + createdAt: account.createdAt, + updatedAt: account.updatedAt, + type: 'bedrock', + hasCredentials: !!account.awsCredentials, + expiresAt: account.expiresAt || null, + subscriptionExpiresAt: account.subscriptionExpiresAt || null + }) + } } // 按优先级和名称排序 @@ -211,6 +234,14 @@ class BedrockAccountService { account.credentialType = updates.credentialType } + if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) { + account.subscriptionExpiresAt = normalizeSubscriptionExpiresAt( + updates.subscriptionExpiresAt + ) + } else if (Object.prototype.hasOwnProperty.call(updates, 'expiresAt')) { + account.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.expiresAt) + } + // 更新AWS凭证 if (updates.awsCredentials !== undefined) { if (updates.awsCredentials) { @@ -245,7 +276,9 @@ class BedrockAccountService { schedulable: account.schedulable, credentialType: account.credentialType, updatedAt: account.updatedAt, - type: 'bedrock' + type: 'bedrock', + expiresAt: account.expiresAt || null, + subscriptionExpiresAt: account.subscriptionExpiresAt || null } } } catch (error) { diff --git a/src/services/ccrAccountService.js b/src/services/ccrAccountService.js index 4b079c23..0672c041 100644 --- a/src/services/ccrAccountService.js +++ b/src/services/ccrAccountService.js @@ -6,6 +6,19 @@ const logger = require('../utils/logger') const config = require('../../config/config') const LRUCache = require('../utils/lruCache') +function normalizeSubscriptionExpiresAt(value) { + if (value === undefined || value === null || value === '') { + return '' + } + + const date = value instanceof Date ? value : new Date(value) + if (Number.isNaN(date.getTime())) { + return '' + } + + return date.toISOString() +} + class CcrAccountService { constructor() { // 加密相关常量 @@ -49,7 +62,8 @@ class CcrAccountService { accountType = 'shared', // 'dedicated' or 'shared' schedulable = true, // 是否可被调度 dailyQuota = 0, // 每日额度限制(美元),0表示不限制 - quotaResetTime = '00:00' // 额度重置时间(HH:mm格式) + quotaResetTime = '00:00', // 额度重置时间(HH:mm格式) + subscriptionExpiresAt = null } = options // 验证必填字段 @@ -91,7 +105,8 @@ class CcrAccountService { // 使用与统计一致的时区日期,避免边界问题 lastResetDate: redis.getDateStringInTimezone(), // 最后重置日期(按配置时区) quotaResetTime, // 额度重置时间 - quotaStoppedAt: '' // 因额度停用的时间 + quotaStoppedAt: '', // 因额度停用的时间 + subscriptionExpiresAt: normalizeSubscriptionExpiresAt(subscriptionExpiresAt) } const client = redis.getClientSafe() @@ -127,7 +142,8 @@ class CcrAccountService { dailyUsage: 0, lastResetDate: accountData.lastResetDate, quotaResetTime, - quotaStoppedAt: null + quotaStoppedAt: null, + subscriptionExpiresAt: accountData.subscriptionExpiresAt || null } } @@ -167,12 +183,14 @@ class CcrAccountService { schedulable: accountData.schedulable !== 'false', // 默认为true,只有明确设置为false才不可调度 // 额度管理相关 dailyQuota: parseFloat(accountData.dailyQuota || '0'), - dailyUsage: parseFloat(accountData.dailyUsage || '0'), - lastResetDate: accountData.lastResetDate || '', - quotaResetTime: accountData.quotaResetTime || '00:00', - quotaStoppedAt: accountData.quotaStoppedAt || null - }) - } + dailyUsage: parseFloat(accountData.dailyUsage || '0'), + lastResetDate: accountData.lastResetDate || '', + quotaResetTime: accountData.quotaResetTime || '00:00', + quotaStoppedAt: accountData.quotaStoppedAt || null, + expiresAt: accountData.expiresAt || null, + subscriptionExpiresAt: accountData.subscriptionExpiresAt || null + }) + } } return accounts @@ -225,6 +243,11 @@ class CcrAccountService { `[DEBUG] Final CCR account data - name: ${accountData.name}, hasApiUrl: ${!!accountData.apiUrl}, hasApiKey: ${!!accountData.apiKey}, supportedModels: ${JSON.stringify(accountData.supportedModels)}` ) + accountData.subscriptionExpiresAt = + accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== '' + ? accountData.subscriptionExpiresAt + : null + return accountData } @@ -288,6 +311,14 @@ class CcrAccountService { updatedData.quotaResetTime = updates.quotaResetTime } + if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) { + updatedData.subscriptionExpiresAt = normalizeSubscriptionExpiresAt( + updates.subscriptionExpiresAt + ) + } else if (Object.prototype.hasOwnProperty.call(updates, 'expiresAt')) { + updatedData.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.expiresAt) + } + await client.hset(`${this.ACCOUNT_KEY_PREFIX}${accountId}`, updatedData) // 处理共享账户集合变更 diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index 365275bf..66f47369 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -185,6 +185,10 @@ class ClaudeAccountService { status: accountData.status, createdAt: accountData.createdAt, expiresAt: accountData.expiresAt, + subscriptionExpiresAt: + accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== '' + ? accountData.subscriptionExpiresAt + : null, scopes: claudeAiOauth ? claudeAiOauth.scopes : [], autoStopOnWarning, useUnifiedUserAgent, @@ -491,7 +495,11 @@ class ClaudeAccountService { createdAt: account.createdAt, lastUsedAt: account.lastUsedAt, lastRefreshAt: account.lastRefreshAt, - expiresAt: account.subscriptionExpiresAt || null, // 账户订阅到期时间 + expiresAt: account.expiresAt || null, + subscriptionExpiresAt: + account.subscriptionExpiresAt && account.subscriptionExpiresAt !== '' + ? account.subscriptionExpiresAt + : null, // 添加 scopes 字段用于判断认证方式 // 处理空字符串的情况,避免返回 [''] scopes: account.scopes && account.scopes.trim() ? account.scopes.split(' ') : [], diff --git a/src/services/claudeConsoleAccountService.js b/src/services/claudeConsoleAccountService.js index 81bb178e..989fc8a5 100644 --- a/src/services/claudeConsoleAccountService.js +++ b/src/services/claudeConsoleAccountService.js @@ -6,6 +6,19 @@ const logger = require('../utils/logger') const config = require('../../config/config') const LRUCache = require('../utils/lruCache') +function normalizeSubscriptionExpiresAt(value) { + if (value === undefined || value === null || value === '') { + return '' + } + + const date = value instanceof Date ? value : new Date(value) + if (Number.isNaN(date.getTime())) { + return '' + } + + return date.toISOString() +} + class ClaudeConsoleAccountService { constructor() { // 加密相关常量 @@ -52,7 +65,8 @@ class ClaudeConsoleAccountService { accountType = 'shared', // 'dedicated' or 'shared' schedulable = true, // 是否可被调度 dailyQuota = 0, // 每日额度限制(美元),0表示不限制 - quotaResetTime = '00:00' // 额度重置时间(HH:mm格式) + quotaResetTime = '00:00', // 额度重置时间(HH:mm格式) + subscriptionExpiresAt = null } = options // 验证必填字段 @@ -94,7 +108,8 @@ class ClaudeConsoleAccountService { // 使用与统计一致的时区日期,避免边界问题 lastResetDate: redis.getDateStringInTimezone(), // 最后重置日期(按配置时区) quotaResetTime, // 额度重置时间 - quotaStoppedAt: '' // 因额度停用的时间 + quotaStoppedAt: '', // 因额度停用的时间 + subscriptionExpiresAt: normalizeSubscriptionExpiresAt(subscriptionExpiresAt) } const client = redis.getClientSafe() @@ -130,7 +145,8 @@ class ClaudeConsoleAccountService { dailyUsage: 0, lastResetDate: accountData.lastResetDate, quotaResetTime, - quotaStoppedAt: null + quotaStoppedAt: null, + subscriptionExpiresAt: accountData.subscriptionExpiresAt || null } } @@ -170,12 +186,14 @@ class ClaudeConsoleAccountService { schedulable: accountData.schedulable !== 'false', // 默认为true,只有明确设置为false才不可调度 // 额度管理相关 dailyQuota: parseFloat(accountData.dailyQuota || '0'), - dailyUsage: parseFloat(accountData.dailyUsage || '0'), - lastResetDate: accountData.lastResetDate || '', - quotaResetTime: accountData.quotaResetTime || '00:00', - quotaStoppedAt: accountData.quotaStoppedAt || null - }) - } + dailyUsage: parseFloat(accountData.dailyUsage || '0'), + lastResetDate: accountData.lastResetDate || '', + quotaResetTime: accountData.quotaResetTime || '00:00', + quotaStoppedAt: accountData.quotaStoppedAt || null, + expiresAt: accountData.expiresAt || null, + subscriptionExpiresAt: accountData.subscriptionExpiresAt || null + }) + } } return accounts @@ -224,6 +242,11 @@ class ClaudeConsoleAccountService { accountData.proxy = JSON.parse(accountData.proxy) } + accountData.subscriptionExpiresAt = + accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== '' + ? accountData.subscriptionExpiresAt + : null + logger.debug( `[DEBUG] Final account data - name: ${accountData.name}, hasApiUrl: ${!!accountData.apiUrl}, hasApiKey: ${!!accountData.apiKey}, supportedModels: ${JSON.stringify(accountData.supportedModels)}` ) @@ -318,6 +341,14 @@ class ClaudeConsoleAccountService { updatedData.quotaStoppedAt = updates.quotaStoppedAt } + if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) { + updatedData.subscriptionExpiresAt = normalizeSubscriptionExpiresAt( + updates.subscriptionExpiresAt + ) + } else if (Object.prototype.hasOwnProperty.call(updates, 'expiresAt')) { + updatedData.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.expiresAt) + } + // 处理账户类型变更 if (updates.accountType && updates.accountType !== existingAccount.accountType) { updatedData.accountType = updates.accountType diff --git a/src/services/geminiAccountService.js b/src/services/geminiAccountService.js index cc28d5ac..1fa9df9b 100644 --- a/src/services/geminiAccountService.js +++ b/src/services/geminiAccountService.js @@ -42,6 +42,19 @@ function generateEncryptionKey() { return _encryptionKeyCache } +function normalizeSubscriptionExpiresAt(value) { + if (value === undefined || value === null || value === '') { + return '' + } + + const date = value instanceof Date ? value : new Date(value) + if (Number.isNaN(date.getTime())) { + return '' + } + + return date.toISOString() +} + // Gemini 账户键前缀 const GEMINI_ACCOUNT_KEY_PREFIX = 'gemini_account:' const SHARED_GEMINI_ACCOUNTS_KEY = 'shared_gemini_accounts' @@ -333,6 +346,10 @@ async function createAccount(accountData) { let refreshToken = '' let expiresAt = '' + const subscriptionExpiresAt = normalizeSubscriptionExpiresAt( + accountData.subscriptionExpiresAt || '' + ) + if (accountData.geminiOauth || accountData.accessToken) { // 如果提供了完整的 OAuth 数据 if (accountData.geminiOauth) { @@ -404,7 +421,8 @@ async function createAccount(accountData) { createdAt: now, updatedAt: now, lastUsedAt: '', - lastRefreshAt: '' + lastRefreshAt: '', + subscriptionExpiresAt } // 保存到 Redis @@ -428,6 +446,10 @@ async function createAccount(accountData) { } } + if (!returnAccount.subscriptionExpiresAt) { + returnAccount.subscriptionExpiresAt = null + } + return returnAccount } @@ -464,6 +486,10 @@ async function getAccount(accountId) { // 转换 schedulable 字符串为布尔值(与 claudeConsoleAccountService 保持一致) accountData.schedulable = accountData.schedulable !== 'false' // 默认为true,只有明确设置为'false'才为false + if (!accountData.subscriptionExpiresAt) { + accountData.subscriptionExpiresAt = null + } + return accountData } @@ -477,6 +503,12 @@ async function updateAccount(accountId, updates) { const now = new Date().toISOString() updates.updatedAt = now + if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) { + updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt( + updates.subscriptionExpiresAt + ) + } + // 检查是否新增了 refresh token // existingAccount.refreshToken 已经是解密后的值了(从 getAccount 返回) const oldRefreshToken = existingAccount.refreshToken || '' @@ -586,6 +618,10 @@ async function updateAccount(accountId, updates) { } } + if (!updatedAccount.subscriptionExpiresAt) { + updatedAccount.subscriptionExpiresAt = null + } + return updatedAccount } @@ -649,6 +685,7 @@ async function getAllAccounts() { geminiOauth: accountData.geminiOauth ? '[ENCRYPTED]' : '', accessToken: accountData.accessToken ? '[ENCRYPTED]' : '', refreshToken: accountData.refreshToken ? '[ENCRYPTED]' : '', + subscriptionExpiresAt: accountData.subscriptionExpiresAt || null, // 添加 scopes 字段用于判断认证方式 // 处理空字符串和默认值的情况 scopes: diff --git a/src/services/openaiAccountService.js b/src/services/openaiAccountService.js index 70f34e65..b85425f1 100644 --- a/src/services/openaiAccountService.js +++ b/src/services/openaiAccountService.js @@ -194,6 +194,19 @@ function buildCodexUsageSnapshot(accountData) { } } +function normalizeSubscriptionExpiresAt(value) { + if (value === undefined || value === null || value === '') { + return '' + } + + const date = value instanceof Date ? value : new Date(value) + if (Number.isNaN(date.getTime())) { + return '' + } + + return date.toISOString() +} + // 刷新访问令牌 async function refreshAccessToken(refreshToken, proxy = null) { try { @@ -517,6 +530,13 @@ async function createAccount(accountData) { // 处理账户信息 const accountInfo = accountData.accountInfo || {} + const tokenExpiresAt = oauthData.expires_in + ? new Date(Date.now() + oauthData.expires_in * 1000).toISOString() + : '' + const subscriptionExpiresAt = normalizeSubscriptionExpiresAt( + accountData.subscriptionExpiresAt || accountInfo.subscriptionExpiresAt || '' + ) + // 检查邮箱是否已经是加密格式(包含冒号分隔的32位十六进制字符) const isEmailEncrypted = accountInfo.email && accountInfo.email.length >= 33 && accountInfo.email.charAt(32) === ':' @@ -553,9 +573,8 @@ async function createAccount(accountData) { email: isEmailEncrypted ? accountInfo.email : encrypt(accountInfo.email || ''), emailVerified: accountInfo.emailVerified === true ? 'true' : 'false', // 过期时间 - expiresAt: oauthData.expires_in - ? new Date(Date.now() + oauthData.expires_in * 1000).toISOString() - : new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(), // 默认1年 + expiresAt: tokenExpiresAt, + subscriptionExpiresAt, // 状态字段 isActive: accountData.isActive !== false ? 'true' : 'false', status: 'active', @@ -580,7 +599,10 @@ async function createAccount(accountData) { } logger.info(`Created OpenAI account: ${accountId}`) - return account + return { + ...account, + subscriptionExpiresAt: account.subscriptionExpiresAt || null + } } // 获取账户 @@ -623,6 +645,11 @@ async function getAccount(accountId) { } } + accountData.subscriptionExpiresAt = + accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== '' + ? accountData.subscriptionExpiresAt + : null + return accountData } @@ -656,6 +683,10 @@ async function updateAccount(accountId, updates) { updates.email = encrypt(updates.email) } + if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) { + updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.subscriptionExpiresAt) + } + // 处理代理配置 if (updates.proxy) { updates.proxy = @@ -688,6 +719,10 @@ async function updateAccount(accountId, updates) { } } + if (!updatedAccount.subscriptionExpiresAt) { + updatedAccount.subscriptionExpiresAt = null + } + return updatedAccount } @@ -770,6 +805,8 @@ async function getAllAccounts() { } } + const subscriptionExpiresAt = accountData.subscriptionExpiresAt || null + // 不解密敏感字段,只返回基本信息 accounts.push({ ...accountData, @@ -784,6 +821,7 @@ async function getAllAccounts() { accountData.scopes && accountData.scopes.trim() ? accountData.scopes.split(' ') : [], // 添加 hasRefreshToken 标记 hasRefreshToken: hasRefreshTokenFlag, + subscriptionExpiresAt, // 添加限流状态信息(统一格式) rateLimitStatus: rateLimitInfo ? { diff --git a/src/services/openaiResponsesAccountService.js b/src/services/openaiResponsesAccountService.js index 061867f3..f5db0d8f 100644 --- a/src/services/openaiResponsesAccountService.js +++ b/src/services/openaiResponsesAccountService.js @@ -5,6 +5,19 @@ const logger = require('../utils/logger') const config = require('../../config/config') const LRUCache = require('../utils/lruCache') +function normalizeSubscriptionExpiresAt(value) { + if (value === undefined || value === null || value === '') { + return '' + } + + const date = value instanceof Date ? value : new Date(value) + if (Number.isNaN(date.getTime())) { + return '' + } + + return date.toISOString() +} + class OpenAIResponsesAccountService { constructor() { // 加密相关常量 @@ -49,7 +62,8 @@ class OpenAIResponsesAccountService { schedulable = true, // 是否可被调度 dailyQuota = 0, // 每日额度限制(美元),0表示不限制 quotaResetTime = '00:00', // 额度重置时间(HH:mm格式) - rateLimitDuration = 60 // 限流时间(分钟) + rateLimitDuration = 60, // 限流时间(分钟) + subscriptionExpiresAt = null } = options // 验证必填字段 @@ -88,7 +102,8 @@ class OpenAIResponsesAccountService { dailyUsage: '0', lastResetDate: redis.getDateStringInTimezone(), quotaResetTime, - quotaStoppedAt: '' + quotaStoppedAt: '', + subscriptionExpiresAt: normalizeSubscriptionExpiresAt(subscriptionExpiresAt) } // 保存到 Redis @@ -98,6 +113,7 @@ class OpenAIResponsesAccountService { return { ...accountData, + subscriptionExpiresAt: accountData.subscriptionExpiresAt || null, apiKey: '***' // 返回时隐藏敏感信息 } } @@ -124,6 +140,11 @@ class OpenAIResponsesAccountService { } } + accountData.subscriptionExpiresAt = + accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== '' + ? accountData.subscriptionExpiresAt + : null + return accountData } @@ -151,6 +172,15 @@ class OpenAIResponsesAccountService { : updates.baseApi } + if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) { + updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt( + updates.subscriptionExpiresAt + ) + } else if (Object.prototype.hasOwnProperty.call(updates, 'expiresAt')) { + updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.expiresAt) + delete updates.expiresAt + } + // 更新 Redis const client = redis.getClientSafe() const key = `${this.ACCOUNT_KEY_PREFIX}${accountId}` @@ -257,6 +287,10 @@ class OpenAIResponsesAccountService { accountData.schedulable = accountData.schedulable !== 'false' // 转换 isActive 字段为布尔值 accountData.isActive = accountData.isActive === 'true' + accountData.subscriptionExpiresAt = + accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== '' + ? accountData.subscriptionExpiresAt + : null accounts.push(accountData) }