fix: 继续修复PR-541遗留的系列bug

This commit is contained in:
shaw
2025-10-12 22:13:38 +08:00
parent 7d4bf9f94f
commit 6f6c274877
9 changed files with 368 additions and 61 deletions

View File

@@ -2267,7 +2267,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
useUnifiedUserAgent, useUnifiedUserAgent,
useUnifiedClientId, useUnifiedClientId,
unifiedClientId, unifiedClientId,
expiresAt expiresAt,
subscriptionExpiresAt
} = req.body } = req.body
if (!name) { if (!name) {
@@ -2311,7 +2312,7 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
useUnifiedUserAgent: useUnifiedUserAgent === true, // 默认为false useUnifiedUserAgent: useUnifiedUserAgent === true, // 默认为false
useUnifiedClientId: useUnifiedClientId === true, // 默认为false useUnifiedClientId: useUnifiedClientId === true, // 默认为false
unifiedClientId: unifiedClientId || '', // 统一的客户端标识 unifiedClientId: unifiedClientId || '', // 统一的客户端标识
expiresAt: expiresAt || null // 账户订阅到期时间 expiresAt: subscriptionExpiresAt ?? expiresAt ?? null // 账户订阅到期时间
}) })
// 如果是分组类型,将账户添加到分组 // 如果是分组类型,将账户添加到分组
@@ -2400,8 +2401,12 @@ router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) =>
// 映射字段名前端的expiresAt -> 后端的subscriptionExpiresAt // 映射字段名前端的expiresAt -> 后端的subscriptionExpiresAt
const mappedUpdates = { ...updates } 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 mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
}
if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
delete mappedUpdates.expiresAt delete mappedUpdates.expiresAt
} }
@@ -2797,8 +2802,12 @@ router.put('/claude-console-accounts/:accountId', authenticateAdmin, async (req,
// 映射字段名前端的expiresAt -> 后端的subscriptionExpiresAt // 映射字段名前端的expiresAt -> 后端的subscriptionExpiresAt
const mappedUpdates = { ...updates } 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 mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
}
if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
delete mappedUpdates.expiresAt delete mappedUpdates.expiresAt
} }
@@ -3214,8 +3223,12 @@ router.put('/ccr-accounts/:accountId', authenticateAdmin, async (req, res) => {
// 映射字段名前端的expiresAt -> 后端的subscriptionExpiresAt // 映射字段名前端的expiresAt -> 后端的subscriptionExpiresAt
const mappedUpdates = { ...updates } 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 mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
}
if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
delete mappedUpdates.expiresAt delete mappedUpdates.expiresAt
} }
@@ -3582,8 +3595,12 @@ router.put('/bedrock-accounts/:accountId', authenticateAdmin, async (req, res) =
// 映射字段名前端的expiresAt -> 后端的subscriptionExpiresAt // 映射字段名前端的expiresAt -> 后端的subscriptionExpiresAt
const mappedUpdates = { ...updates } 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 mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
}
if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
delete mappedUpdates.expiresAt delete mappedUpdates.expiresAt
} }
@@ -3912,8 +3929,11 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
return { return {
...account, ...account,
// 映射字段:使用 subscriptionExpiresAt 作为前端显示的 expiresAt expiresAt: account.expiresAt || null,
expiresAt: account.subscriptionExpiresAt || null, subscriptionExpiresAt:
account.subscriptionExpiresAt && account.subscriptionExpiresAt !== ''
? account.subscriptionExpiresAt
: null,
groupInfos, groupInfos,
usage: { usage: {
daily: usageStats.daily, daily: usageStats.daily,
@@ -3931,8 +3951,11 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
const groupInfos = await accountGroupService.getAccountGroups(account.id) const groupInfos = await accountGroupService.getAccountGroups(account.id)
return { return {
...account, ...account,
// 映射字段:使用 subscriptionExpiresAt 作为前端显示的 expiresAt expiresAt: account.expiresAt || null,
expiresAt: account.subscriptionExpiresAt || null, subscriptionExpiresAt:
account.subscriptionExpiresAt && account.subscriptionExpiresAt !== ''
? account.subscriptionExpiresAt
: null,
groupInfos, groupInfos,
usage: { usage: {
daily: { tokens: 0, requests: 0, allTokens: 0 }, daily: { tokens: 0, requests: 0, allTokens: 0 },
@@ -3947,6 +3970,11 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
) )
return { return {
...account, ...account,
expiresAt: account.expiresAt || null,
subscriptionExpiresAt:
account.subscriptionExpiresAt && account.subscriptionExpiresAt !== ''
? account.subscriptionExpiresAt
: null,
groupInfos: [], groupInfos: [],
usage: { usage: {
daily: { tokens: 0, requests: 0, allTokens: 0 }, daily: { tokens: 0, requests: 0, allTokens: 0 },
@@ -4059,8 +4087,12 @@ router.put('/gemini-accounts/:accountId', authenticateAdmin, async (req, res) =>
// 映射字段名前端的expiresAt -> 后端的subscriptionExpiresAt // 映射字段名前端的expiresAt -> 后端的subscriptionExpiresAt
const mappedUpdates = { ...updates } 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 mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
}
if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
delete mappedUpdates.expiresAt delete mappedUpdates.expiresAt
} }
@@ -7270,7 +7302,8 @@ router.post('/openai-accounts', authenticateAdmin, async (req, res) => {
rateLimitDuration, rateLimitDuration,
priority, priority,
needsImmediateRefresh, // 是否需要立即刷新 needsImmediateRefresh, // 是否需要立即刷新
requireRefreshSuccess // 是否必须刷新成功才能创建 requireRefreshSuccess, // 是否必须刷新成功才能创建
subscriptionExpiresAt
} = req.body } = req.body
if (!name) { if (!name) {
@@ -7292,7 +7325,8 @@ router.post('/openai-accounts', authenticateAdmin, async (req, res) => {
accountInfo: accountInfo || {}, accountInfo: accountInfo || {},
proxy: proxy || null, proxy: proxy || null,
isActive: true, isActive: true,
schedulable: true schedulable: true,
subscriptionExpiresAt: subscriptionExpiresAt || null
} }
// 如果需要立即刷新且必须成功OpenAI 手动模式) // 如果需要立即刷新且必须成功OpenAI 手动模式)
@@ -7571,10 +7605,16 @@ router.put('/openai-accounts/:id', authenticateAdmin, async (req, res) => {
: currentAccount.emailVerified : currentAccount.emailVerified
} }
// 映射字段名前端的expiresAt -> 后端的subscriptionExpiresAt (订阅过期时间) const hasOauthExpiry = Boolean(updates.openaiOauth?.expires_in)
// 注意:这里不影响上面 OAuth token 的 expiresAt 字段
if ('expiresAt' in updates && !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 updateData.subscriptionExpiresAt = updates.expiresAt
}
if (!hasOauthExpiry && Object.prototype.hasOwnProperty.call(updateData, 'subscriptionExpiresAt')) {
delete updateData.expiresAt delete updateData.expiresAt
} }
@@ -7966,8 +8006,12 @@ router.put('/azure-openai-accounts/:id', authenticateAdmin, async (req, res) =>
// 映射字段名前端的expiresAt -> 后端的subscriptionExpiresAt // 映射字段名前端的expiresAt -> 后端的subscriptionExpiresAt
const mappedUpdates = { ...updates } 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 mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
}
if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
delete mappedUpdates.expiresAt delete mappedUpdates.expiresAt
} }
@@ -8346,8 +8390,12 @@ router.put('/openai-responses-accounts/:id', authenticateAdmin, async (req, res)
// 映射字段名前端的expiresAt -> 后端的subscriptionExpiresAt // 映射字段名前端的expiresAt -> 后端的subscriptionExpiresAt
const mappedUpdates = { ...updates } 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 mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
}
if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
delete mappedUpdates.expiresAt delete mappedUpdates.expiresAt
} }
@@ -8717,9 +8765,11 @@ router.get('/droid-accounts', authenticateAdmin, async (req, res) => {
return { return {
...account, ...account,
// 映射字段:使用 subscriptionExpiresAt 作为前端显示的 expiresAt expiresAt: account.expiresAt || null,
// OAuth token 的原始 expiresAt 保留在内部使用 subscriptionExpiresAt:
expiresAt: account.subscriptionExpiresAt || null, account.subscriptionExpiresAt && account.subscriptionExpiresAt !== ''
? account.subscriptionExpiresAt
: null,
schedulable: account.schedulable === 'true', schedulable: account.schedulable === 'true',
boundApiKeysCount, boundApiKeysCount,
groupInfos, 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) logger.warn(`Failed to get stats for Droid account ${account.id}:`, error.message)
return { return {
...account, ...account,
// 映射字段:使用 subscriptionExpiresAt 作为前端显示的 expiresAt expiresAt: account.expiresAt || null,
expiresAt: account.subscriptionExpiresAt || null, subscriptionExpiresAt:
account.subscriptionExpiresAt && account.subscriptionExpiresAt !== ''
? account.subscriptionExpiresAt
: null,
boundApiKeysCount: 0, boundApiKeysCount: 0,
groupInfos: [], groupInfos: [],
usage: { usage: {
@@ -8851,8 +8904,12 @@ router.put('/droid-accounts/:id', authenticateAdmin, async (req, res) => {
// 映射字段名前端的expiresAt -> 后端的subscriptionExpiresAt // 映射字段名前端的expiresAt -> 后端的subscriptionExpiresAt
const mappedUpdates = { ...updates } 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 mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
}
if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
delete mappedUpdates.expiresAt delete mappedUpdates.expiresAt
} }

View File

@@ -65,6 +65,19 @@ const AZURE_OPENAI_ACCOUNT_KEY_PREFIX = 'azure_openai:account:'
const SHARED_AZURE_OPENAI_ACCOUNTS_KEY = 'shared_azure_openai_accounts' const SHARED_AZURE_OPENAI_ACCOUNTS_KEY = 'shared_azure_openai_accounts'
const ACCOUNT_SESSION_MAPPING_PREFIX = 'azure_openai_session_account_mapping:' 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) { function encrypt(text) {
if (!text) { if (!text) {
@@ -133,6 +146,9 @@ async function createAccount(accountData) {
isActive: accountData.isActive !== false ? 'true' : 'false', isActive: accountData.isActive !== false ? 'true' : 'false',
status: 'active', status: 'active',
schedulable: accountData.schedulable !== false ? 'true' : 'false', schedulable: accountData.schedulable !== false ? 'true' : 'false',
subscriptionExpiresAt: normalizeSubscriptionExpiresAt(
accountData.subscriptionExpiresAt || ''
),
createdAt: now, createdAt: now,
updatedAt: now updatedAt: now
} }
@@ -152,7 +168,10 @@ async function createAccount(accountData) {
} }
logger.info(`Created Azure OpenAI account: ${accountId}`) 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 return accountData
} }
@@ -218,6 +242,15 @@ async function updateAccount(accountId, updates) {
: JSON.stringify(updates.supportedModels) : 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() const client = redisClient.getClientSafe()
if (updates.accountType && updates.accountType !== existingAccount.accountType) { if (updates.accountType && updates.accountType !== existingAccount.accountType) {
@@ -244,6 +277,10 @@ async function updateAccount(accountId, updates) {
} }
} }
if (!updatedAccount.subscriptionExpiresAt) {
updatedAccount.subscriptionExpiresAt = null
}
return updatedAccount return updatedAccount
} }
@@ -303,7 +340,8 @@ async function getAllAccounts() {
accounts.push({ accounts.push({
...accountData, ...accountData,
isActive: accountData.isActive === 'true', isActive: accountData.isActive === 'true',
schedulable: accountData.schedulable !== 'false' schedulable: accountData.schedulable !== 'false',
subscriptionExpiresAt: accountData.subscriptionExpiresAt || null
}) })
} }
} }

View File

@@ -6,6 +6,19 @@ const config = require('../../config/config')
const bedrockRelayService = require('./bedrockRelayService') const bedrockRelayService = require('./bedrockRelayService')
const LRUCache = require('../utils/lruCache') 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 { class BedrockAccountService {
constructor() { constructor() {
// 加密相关常量 // 加密相关常量
@@ -40,7 +53,8 @@ class BedrockAccountService {
accountType = 'shared', // 'dedicated' or 'shared' accountType = 'shared', // 'dedicated' or 'shared'
priority = 50, // 调度优先级 (1-100数字越小优先级越高) priority = 50, // 调度优先级 (1-100数字越小优先级越高)
schedulable = true, // 是否可被调度 schedulable = true, // 是否可被调度
credentialType = 'default' // 'default', 'access_key', 'bearer_token' credentialType = 'default', // 'default', 'access_key', 'bearer_token'
subscriptionExpiresAt = null
} = options } = options
const accountId = uuidv4() const accountId = uuidv4()
@@ -56,6 +70,7 @@ class BedrockAccountService {
priority, priority,
schedulable, schedulable,
credentialType, credentialType,
subscriptionExpiresAt: normalizeSubscriptionExpiresAt(subscriptionExpiresAt),
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
type: 'bedrock' // 标识这是Bedrock账户 type: 'bedrock' // 标识这是Bedrock账户
@@ -84,6 +99,7 @@ class BedrockAccountService {
priority, priority,
schedulable, schedulable,
credentialType, credentialType,
subscriptionExpiresAt: accountData.subscriptionExpiresAt || null,
createdAt: accountData.createdAt, createdAt: accountData.createdAt,
type: 'bedrock' type: 'bedrock'
} }
@@ -106,6 +122,11 @@ class BedrockAccountService {
account.awsCredentials = this._decryptAwsCredentials(account.awsCredentials) account.awsCredentials = this._decryptAwsCredentials(account.awsCredentials)
} }
account.subscriptionExpiresAt =
account.subscriptionExpiresAt && account.subscriptionExpiresAt !== ''
? account.subscriptionExpiresAt
: null
logger.debug(`🔍 获取Bedrock账户 - ID: ${accountId}, 名称: ${account.name}`) logger.debug(`🔍 获取Bedrock账户 - ID: ${accountId}, 名称: ${account.name}`)
return { return {
@@ -141,13 +162,15 @@ class BedrockAccountService {
accountType: account.accountType, accountType: account.accountType,
priority: account.priority, priority: account.priority,
schedulable: account.schedulable, schedulable: account.schedulable,
credentialType: account.credentialType, credentialType: account.credentialType,
createdAt: account.createdAt, createdAt: account.createdAt,
updatedAt: account.updatedAt, updatedAt: account.updatedAt,
type: 'bedrock', type: 'bedrock',
hasCredentials: !!account.awsCredentials hasCredentials: !!account.awsCredentials,
}) expiresAt: account.expiresAt || null,
} subscriptionExpiresAt: account.subscriptionExpiresAt || null
})
}
} }
// 按优先级和名称排序 // 按优先级和名称排序
@@ -211,6 +234,14 @@ class BedrockAccountService {
account.credentialType = updates.credentialType 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凭证 // 更新AWS凭证
if (updates.awsCredentials !== undefined) { if (updates.awsCredentials !== undefined) {
if (updates.awsCredentials) { if (updates.awsCredentials) {
@@ -245,7 +276,9 @@ class BedrockAccountService {
schedulable: account.schedulable, schedulable: account.schedulable,
credentialType: account.credentialType, credentialType: account.credentialType,
updatedAt: account.updatedAt, updatedAt: account.updatedAt,
type: 'bedrock' type: 'bedrock',
expiresAt: account.expiresAt || null,
subscriptionExpiresAt: account.subscriptionExpiresAt || null
} }
} }
} catch (error) { } catch (error) {

View File

@@ -6,6 +6,19 @@ const logger = require('../utils/logger')
const config = require('../../config/config') const config = require('../../config/config')
const LRUCache = require('../utils/lruCache') 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 { class CcrAccountService {
constructor() { constructor() {
// 加密相关常量 // 加密相关常量
@@ -49,7 +62,8 @@ class CcrAccountService {
accountType = 'shared', // 'dedicated' or 'shared' accountType = 'shared', // 'dedicated' or 'shared'
schedulable = true, // 是否可被调度 schedulable = true, // 是否可被调度
dailyQuota = 0, // 每日额度限制美元0表示不限制 dailyQuota = 0, // 每日额度限制美元0表示不限制
quotaResetTime = '00:00' // 额度重置时间HH:mm格式 quotaResetTime = '00:00', // 额度重置时间HH:mm格式
subscriptionExpiresAt = null
} = options } = options
// 验证必填字段 // 验证必填字段
@@ -91,7 +105,8 @@ class CcrAccountService {
// 使用与统计一致的时区日期,避免边界问题 // 使用与统计一致的时区日期,避免边界问题
lastResetDate: redis.getDateStringInTimezone(), // 最后重置日期(按配置时区) lastResetDate: redis.getDateStringInTimezone(), // 最后重置日期(按配置时区)
quotaResetTime, // 额度重置时间 quotaResetTime, // 额度重置时间
quotaStoppedAt: '' // 因额度停用的时间 quotaStoppedAt: '', // 因额度停用的时间
subscriptionExpiresAt: normalizeSubscriptionExpiresAt(subscriptionExpiresAt)
} }
const client = redis.getClientSafe() const client = redis.getClientSafe()
@@ -127,7 +142,8 @@ class CcrAccountService {
dailyUsage: 0, dailyUsage: 0,
lastResetDate: accountData.lastResetDate, lastResetDate: accountData.lastResetDate,
quotaResetTime, quotaResetTime,
quotaStoppedAt: null quotaStoppedAt: null,
subscriptionExpiresAt: accountData.subscriptionExpiresAt || null
} }
} }
@@ -167,12 +183,14 @@ class CcrAccountService {
schedulable: accountData.schedulable !== 'false', // 默认为true只有明确设置为false才不可调度 schedulable: accountData.schedulable !== 'false', // 默认为true只有明确设置为false才不可调度
// 额度管理相关 // 额度管理相关
dailyQuota: parseFloat(accountData.dailyQuota || '0'), dailyQuota: parseFloat(accountData.dailyQuota || '0'),
dailyUsage: parseFloat(accountData.dailyUsage || '0'), dailyUsage: parseFloat(accountData.dailyUsage || '0'),
lastResetDate: accountData.lastResetDate || '', lastResetDate: accountData.lastResetDate || '',
quotaResetTime: accountData.quotaResetTime || '00:00', quotaResetTime: accountData.quotaResetTime || '00:00',
quotaStoppedAt: accountData.quotaStoppedAt || null quotaStoppedAt: accountData.quotaStoppedAt || null,
}) expiresAt: accountData.expiresAt || null,
} subscriptionExpiresAt: accountData.subscriptionExpiresAt || null
})
}
} }
return accounts 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)}` `[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 return accountData
} }
@@ -288,6 +311,14 @@ class CcrAccountService {
updatedData.quotaResetTime = updates.quotaResetTime 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) await client.hset(`${this.ACCOUNT_KEY_PREFIX}${accountId}`, updatedData)
// 处理共享账户集合变更 // 处理共享账户集合变更

View File

@@ -185,6 +185,10 @@ class ClaudeAccountService {
status: accountData.status, status: accountData.status,
createdAt: accountData.createdAt, createdAt: accountData.createdAt,
expiresAt: accountData.expiresAt, expiresAt: accountData.expiresAt,
subscriptionExpiresAt:
accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== ''
? accountData.subscriptionExpiresAt
: null,
scopes: claudeAiOauth ? claudeAiOauth.scopes : [], scopes: claudeAiOauth ? claudeAiOauth.scopes : [],
autoStopOnWarning, autoStopOnWarning,
useUnifiedUserAgent, useUnifiedUserAgent,
@@ -491,7 +495,11 @@ class ClaudeAccountService {
createdAt: account.createdAt, createdAt: account.createdAt,
lastUsedAt: account.lastUsedAt, lastUsedAt: account.lastUsedAt,
lastRefreshAt: account.lastRefreshAt, lastRefreshAt: account.lastRefreshAt,
expiresAt: account.subscriptionExpiresAt || null, // 账户订阅到期时间 expiresAt: account.expiresAt || null,
subscriptionExpiresAt:
account.subscriptionExpiresAt && account.subscriptionExpiresAt !== ''
? account.subscriptionExpiresAt
: null,
// 添加 scopes 字段用于判断认证方式 // 添加 scopes 字段用于判断认证方式
// 处理空字符串的情况,避免返回 [''] // 处理空字符串的情况,避免返回 ['']
scopes: account.scopes && account.scopes.trim() ? account.scopes.split(' ') : [], scopes: account.scopes && account.scopes.trim() ? account.scopes.split(' ') : [],

View File

@@ -6,6 +6,19 @@ const logger = require('../utils/logger')
const config = require('../../config/config') const config = require('../../config/config')
const LRUCache = require('../utils/lruCache') 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 { class ClaudeConsoleAccountService {
constructor() { constructor() {
// 加密相关常量 // 加密相关常量
@@ -52,7 +65,8 @@ class ClaudeConsoleAccountService {
accountType = 'shared', // 'dedicated' or 'shared' accountType = 'shared', // 'dedicated' or 'shared'
schedulable = true, // 是否可被调度 schedulable = true, // 是否可被调度
dailyQuota = 0, // 每日额度限制美元0表示不限制 dailyQuota = 0, // 每日额度限制美元0表示不限制
quotaResetTime = '00:00' // 额度重置时间HH:mm格式 quotaResetTime = '00:00', // 额度重置时间HH:mm格式
subscriptionExpiresAt = null
} = options } = options
// 验证必填字段 // 验证必填字段
@@ -94,7 +108,8 @@ class ClaudeConsoleAccountService {
// 使用与统计一致的时区日期,避免边界问题 // 使用与统计一致的时区日期,避免边界问题
lastResetDate: redis.getDateStringInTimezone(), // 最后重置日期(按配置时区) lastResetDate: redis.getDateStringInTimezone(), // 最后重置日期(按配置时区)
quotaResetTime, // 额度重置时间 quotaResetTime, // 额度重置时间
quotaStoppedAt: '' // 因额度停用的时间 quotaStoppedAt: '', // 因额度停用的时间
subscriptionExpiresAt: normalizeSubscriptionExpiresAt(subscriptionExpiresAt)
} }
const client = redis.getClientSafe() const client = redis.getClientSafe()
@@ -130,7 +145,8 @@ class ClaudeConsoleAccountService {
dailyUsage: 0, dailyUsage: 0,
lastResetDate: accountData.lastResetDate, lastResetDate: accountData.lastResetDate,
quotaResetTime, quotaResetTime,
quotaStoppedAt: null quotaStoppedAt: null,
subscriptionExpiresAt: accountData.subscriptionExpiresAt || null
} }
} }
@@ -170,12 +186,14 @@ class ClaudeConsoleAccountService {
schedulable: accountData.schedulable !== 'false', // 默认为true只有明确设置为false才不可调度 schedulable: accountData.schedulable !== 'false', // 默认为true只有明确设置为false才不可调度
// 额度管理相关 // 额度管理相关
dailyQuota: parseFloat(accountData.dailyQuota || '0'), dailyQuota: parseFloat(accountData.dailyQuota || '0'),
dailyUsage: parseFloat(accountData.dailyUsage || '0'), dailyUsage: parseFloat(accountData.dailyUsage || '0'),
lastResetDate: accountData.lastResetDate || '', lastResetDate: accountData.lastResetDate || '',
quotaResetTime: accountData.quotaResetTime || '00:00', quotaResetTime: accountData.quotaResetTime || '00:00',
quotaStoppedAt: accountData.quotaStoppedAt || null quotaStoppedAt: accountData.quotaStoppedAt || null,
}) expiresAt: accountData.expiresAt || null,
} subscriptionExpiresAt: accountData.subscriptionExpiresAt || null
})
}
} }
return accounts return accounts
@@ -224,6 +242,11 @@ class ClaudeConsoleAccountService {
accountData.proxy = JSON.parse(accountData.proxy) accountData.proxy = JSON.parse(accountData.proxy)
} }
accountData.subscriptionExpiresAt =
accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== ''
? accountData.subscriptionExpiresAt
: null
logger.debug( logger.debug(
`[DEBUG] Final account data - name: ${accountData.name}, hasApiUrl: ${!!accountData.apiUrl}, hasApiKey: ${!!accountData.apiKey}, supportedModels: ${JSON.stringify(accountData.supportedModels)}` `[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 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) { if (updates.accountType && updates.accountType !== existingAccount.accountType) {
updatedData.accountType = updates.accountType updatedData.accountType = updates.accountType

View File

@@ -42,6 +42,19 @@ function generateEncryptionKey() {
return _encryptionKeyCache 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 账户键前缀 // Gemini 账户键前缀
const GEMINI_ACCOUNT_KEY_PREFIX = 'gemini_account:' const GEMINI_ACCOUNT_KEY_PREFIX = 'gemini_account:'
const SHARED_GEMINI_ACCOUNTS_KEY = 'shared_gemini_accounts' const SHARED_GEMINI_ACCOUNTS_KEY = 'shared_gemini_accounts'
@@ -333,6 +346,10 @@ async function createAccount(accountData) {
let refreshToken = '' let refreshToken = ''
let expiresAt = '' let expiresAt = ''
const subscriptionExpiresAt = normalizeSubscriptionExpiresAt(
accountData.subscriptionExpiresAt || ''
)
if (accountData.geminiOauth || accountData.accessToken) { if (accountData.geminiOauth || accountData.accessToken) {
// 如果提供了完整的 OAuth 数据 // 如果提供了完整的 OAuth 数据
if (accountData.geminiOauth) { if (accountData.geminiOauth) {
@@ -404,7 +421,8 @@ async function createAccount(accountData) {
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
lastUsedAt: '', lastUsedAt: '',
lastRefreshAt: '' lastRefreshAt: '',
subscriptionExpiresAt
} }
// 保存到 Redis // 保存到 Redis
@@ -428,6 +446,10 @@ async function createAccount(accountData) {
} }
} }
if (!returnAccount.subscriptionExpiresAt) {
returnAccount.subscriptionExpiresAt = null
}
return returnAccount return returnAccount
} }
@@ -464,6 +486,10 @@ async function getAccount(accountId) {
// 转换 schedulable 字符串为布尔值(与 claudeConsoleAccountService 保持一致) // 转换 schedulable 字符串为布尔值(与 claudeConsoleAccountService 保持一致)
accountData.schedulable = accountData.schedulable !== 'false' // 默认为true只有明确设置为'false'才为false accountData.schedulable = accountData.schedulable !== 'false' // 默认为true只有明确设置为'false'才为false
if (!accountData.subscriptionExpiresAt) {
accountData.subscriptionExpiresAt = null
}
return accountData return accountData
} }
@@ -477,6 +503,12 @@ async function updateAccount(accountId, updates) {
const now = new Date().toISOString() const now = new Date().toISOString()
updates.updatedAt = now updates.updatedAt = now
if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(
updates.subscriptionExpiresAt
)
}
// 检查是否新增了 refresh token // 检查是否新增了 refresh token
// existingAccount.refreshToken 已经是解密后的值了(从 getAccount 返回) // existingAccount.refreshToken 已经是解密后的值了(从 getAccount 返回)
const oldRefreshToken = existingAccount.refreshToken || '' const oldRefreshToken = existingAccount.refreshToken || ''
@@ -586,6 +618,10 @@ async function updateAccount(accountId, updates) {
} }
} }
if (!updatedAccount.subscriptionExpiresAt) {
updatedAccount.subscriptionExpiresAt = null
}
return updatedAccount return updatedAccount
} }
@@ -649,6 +685,7 @@ async function getAllAccounts() {
geminiOauth: accountData.geminiOauth ? '[ENCRYPTED]' : '', geminiOauth: accountData.geminiOauth ? '[ENCRYPTED]' : '',
accessToken: accountData.accessToken ? '[ENCRYPTED]' : '', accessToken: accountData.accessToken ? '[ENCRYPTED]' : '',
refreshToken: accountData.refreshToken ? '[ENCRYPTED]' : '', refreshToken: accountData.refreshToken ? '[ENCRYPTED]' : '',
subscriptionExpiresAt: accountData.subscriptionExpiresAt || null,
// 添加 scopes 字段用于判断认证方式 // 添加 scopes 字段用于判断认证方式
// 处理空字符串和默认值的情况 // 处理空字符串和默认值的情况
scopes: scopes:

View File

@@ -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) { async function refreshAccessToken(refreshToken, proxy = null) {
try { try {
@@ -517,6 +530,13 @@ async function createAccount(accountData) {
// 处理账户信息 // 处理账户信息
const accountInfo = accountData.accountInfo || {} 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位十六进制字符 // 检查邮箱是否已经是加密格式包含冒号分隔的32位十六进制字符
const isEmailEncrypted = const isEmailEncrypted =
accountInfo.email && accountInfo.email.length >= 33 && accountInfo.email.charAt(32) === ':' 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 || ''), email: isEmailEncrypted ? accountInfo.email : encrypt(accountInfo.email || ''),
emailVerified: accountInfo.emailVerified === true ? 'true' : 'false', emailVerified: accountInfo.emailVerified === true ? 'true' : 'false',
// 过期时间 // 过期时间
expiresAt: oauthData.expires_in expiresAt: tokenExpiresAt,
? new Date(Date.now() + oauthData.expires_in * 1000).toISOString() subscriptionExpiresAt,
: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(), // 默认1年
// 状态字段 // 状态字段
isActive: accountData.isActive !== false ? 'true' : 'false', isActive: accountData.isActive !== false ? 'true' : 'false',
status: 'active', status: 'active',
@@ -580,7 +599,10 @@ async function createAccount(accountData) {
} }
logger.info(`Created OpenAI account: ${accountId}`) 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 return accountData
} }
@@ -656,6 +683,10 @@ async function updateAccount(accountId, updates) {
updates.email = encrypt(updates.email) updates.email = encrypt(updates.email)
} }
if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.subscriptionExpiresAt)
}
// 处理代理配置 // 处理代理配置
if (updates.proxy) { if (updates.proxy) {
updates.proxy = updates.proxy =
@@ -688,6 +719,10 @@ async function updateAccount(accountId, updates) {
} }
} }
if (!updatedAccount.subscriptionExpiresAt) {
updatedAccount.subscriptionExpiresAt = null
}
return updatedAccount return updatedAccount
} }
@@ -770,6 +805,8 @@ async function getAllAccounts() {
} }
} }
const subscriptionExpiresAt = accountData.subscriptionExpiresAt || null
// 不解密敏感字段,只返回基本信息 // 不解密敏感字段,只返回基本信息
accounts.push({ accounts.push({
...accountData, ...accountData,
@@ -784,6 +821,7 @@ async function getAllAccounts() {
accountData.scopes && accountData.scopes.trim() ? accountData.scopes.split(' ') : [], accountData.scopes && accountData.scopes.trim() ? accountData.scopes.split(' ') : [],
// 添加 hasRefreshToken 标记 // 添加 hasRefreshToken 标记
hasRefreshToken: hasRefreshTokenFlag, hasRefreshToken: hasRefreshTokenFlag,
subscriptionExpiresAt,
// 添加限流状态信息(统一格式) // 添加限流状态信息(统一格式)
rateLimitStatus: rateLimitInfo rateLimitStatus: rateLimitInfo
? { ? {

View File

@@ -5,6 +5,19 @@ const logger = require('../utils/logger')
const config = require('../../config/config') const config = require('../../config/config')
const LRUCache = require('../utils/lruCache') 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 { class OpenAIResponsesAccountService {
constructor() { constructor() {
// 加密相关常量 // 加密相关常量
@@ -49,7 +62,8 @@ class OpenAIResponsesAccountService {
schedulable = true, // 是否可被调度 schedulable = true, // 是否可被调度
dailyQuota = 0, // 每日额度限制美元0表示不限制 dailyQuota = 0, // 每日额度限制美元0表示不限制
quotaResetTime = '00:00', // 额度重置时间HH:mm格式 quotaResetTime = '00:00', // 额度重置时间HH:mm格式
rateLimitDuration = 60 // 限流时间(分钟) rateLimitDuration = 60, // 限流时间(分钟)
subscriptionExpiresAt = null
} = options } = options
// 验证必填字段 // 验证必填字段
@@ -88,7 +102,8 @@ class OpenAIResponsesAccountService {
dailyUsage: '0', dailyUsage: '0',
lastResetDate: redis.getDateStringInTimezone(), lastResetDate: redis.getDateStringInTimezone(),
quotaResetTime, quotaResetTime,
quotaStoppedAt: '' quotaStoppedAt: '',
subscriptionExpiresAt: normalizeSubscriptionExpiresAt(subscriptionExpiresAt)
} }
// 保存到 Redis // 保存到 Redis
@@ -98,6 +113,7 @@ class OpenAIResponsesAccountService {
return { return {
...accountData, ...accountData,
subscriptionExpiresAt: accountData.subscriptionExpiresAt || null,
apiKey: '***' // 返回时隐藏敏感信息 apiKey: '***' // 返回时隐藏敏感信息
} }
} }
@@ -124,6 +140,11 @@ class OpenAIResponsesAccountService {
} }
} }
accountData.subscriptionExpiresAt =
accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== ''
? accountData.subscriptionExpiresAt
: null
return accountData return accountData
} }
@@ -151,6 +172,15 @@ class OpenAIResponsesAccountService {
: updates.baseApi : 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 // 更新 Redis
const client = redis.getClientSafe() const client = redis.getClientSafe()
const key = `${this.ACCOUNT_KEY_PREFIX}${accountId}` const key = `${this.ACCOUNT_KEY_PREFIX}${accountId}`
@@ -257,6 +287,10 @@ class OpenAIResponsesAccountService {
accountData.schedulable = accountData.schedulable !== 'false' accountData.schedulable = accountData.schedulable !== 'false'
// 转换 isActive 字段为布尔值 // 转换 isActive 字段为布尔值
accountData.isActive = accountData.isActive === 'true' accountData.isActive = accountData.isActive === 'true'
accountData.subscriptionExpiresAt =
accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== ''
? accountData.subscriptionExpiresAt
: null
accounts.push(accountData) accounts.push(accountData)
} }