hequ commited on
Commit
497686c
·
verified ·
1 Parent(s): f949e55

Upload 220 files

Browse files
VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.175
 
1
+ 1.1.176
src/app.js CHANGED
@@ -14,6 +14,7 @@ const cacheMonitor = require('./utils/cacheMonitor')
14
 
15
  // Import routes
16
  const apiRoutes = require('./routes/api')
 
17
  const adminRoutes = require('./routes/admin')
18
  const webRoutes = require('./routes/web')
19
  const apiStatsRoutes = require('./routes/apiStats')
@@ -55,6 +56,11 @@ class Application {
55
  logger.info('🔄 Initializing pricing service...')
56
  await pricingService.initialize()
57
 
 
 
 
 
 
58
  // 📊 初始化缓存监控
59
  await this.initializeCacheMonitoring()
60
 
@@ -251,6 +257,7 @@ class Application {
251
 
252
  // 🛣️ 路由
253
  this.app.use('/api', apiRoutes)
 
254
  this.app.use('/claude', apiRoutes) // /claude 路由别名,与 /api 功能相同
255
  this.app.use('/admin', adminRoutes)
256
  this.app.use('/users', userRoutes)
@@ -262,7 +269,8 @@ class Application {
262
  this.app.use('/gemini', geminiRoutes) // 保留原有路径以保持向后兼容
263
  this.app.use('/openai/gemini', openaiGeminiRoutes)
264
  this.app.use('/openai/claude', openaiClaudeRoutes)
265
- this.app.use('/openai', openaiRoutes)
 
266
  // Droid 路由:支持多种 Factory.ai 端点
267
  this.app.use('/droid', droidRoutes) // Droid (Factory.ai) API 转发
268
  this.app.use('/azure', azureOpenaiRoutes)
@@ -630,6 +638,15 @@ class Application {
630
  logger.error('❌ Error cleaning up pricing service:', error)
631
  }
632
 
 
 
 
 
 
 
 
 
 
633
  // 停止限流清理服务
634
  try {
635
  const rateLimitCleanupService = require('./services/rateLimitCleanupService')
 
14
 
15
  // Import routes
16
  const apiRoutes = require('./routes/api')
17
+ const unifiedRoutes = require('./routes/unified')
18
  const adminRoutes = require('./routes/admin')
19
  const webRoutes = require('./routes/web')
20
  const apiStatsRoutes = require('./routes/apiStats')
 
56
  logger.info('🔄 Initializing pricing service...')
57
  await pricingService.initialize()
58
 
59
+ // 📋 初始化模型服务
60
+ logger.info('🔄 Initializing model service...')
61
+ const modelService = require('./services/modelService')
62
+ await modelService.initialize()
63
+
64
  // 📊 初始化缓存监控
65
  await this.initializeCacheMonitoring()
66
 
 
257
 
258
  // 🛣️ 路由
259
  this.app.use('/api', apiRoutes)
260
+ this.app.use('/api', unifiedRoutes) // 统一智能路由(支持 /v1/chat/completions 等)
261
  this.app.use('/claude', apiRoutes) // /claude 路由别名,与 /api 功能相同
262
  this.app.use('/admin', adminRoutes)
263
  this.app.use('/users', userRoutes)
 
269
  this.app.use('/gemini', geminiRoutes) // 保留原有路径以保持向后兼容
270
  this.app.use('/openai/gemini', openaiGeminiRoutes)
271
  this.app.use('/openai/claude', openaiClaudeRoutes)
272
+ this.app.use('/openai', unifiedRoutes) // 复用统一智能路由,支持 /openai/v1/chat/completions
273
+ this.app.use('/openai', openaiRoutes) // Codex API 路由(/openai/responses, /openai/v1/responses)
274
  // Droid 路由:支持多种 Factory.ai 端点
275
  this.app.use('/droid', droidRoutes) // Droid (Factory.ai) API 转发
276
  this.app.use('/azure', azureOpenaiRoutes)
 
638
  logger.error('❌ Error cleaning up pricing service:', error)
639
  }
640
 
641
+ // 清理 model service 的文件监听器
642
+ try {
643
+ const modelService = require('./services/modelService')
644
+ modelService.cleanup()
645
+ logger.info('📋 Model service cleaned up')
646
+ } catch (error) {
647
+ logger.error('❌ Error cleaning up model service:', error)
648
+ }
649
+
650
  // 停止限流清理服务
651
  try {
652
  const rateLimitCleanupService = require('./services/rateLimitCleanupService')
src/middleware/auth.js CHANGED
@@ -61,8 +61,7 @@ const resolveConcurrencyConfig = () => {
61
  const TOKEN_COUNT_PATHS = new Set([
62
  '/v1/messages/count_tokens',
63
  '/api/v1/messages/count_tokens',
64
- '/claude/v1/messages/count_tokens',
65
- '/droid/claude/v1/messages/count_tokens'
66
  ])
67
 
68
  function extractApiKey(req) {
 
61
  const TOKEN_COUNT_PATHS = new Set([
62
  '/v1/messages/count_tokens',
63
  '/api/v1/messages/count_tokens',
64
+ '/claude/v1/messages/count_tokens'
 
65
  ])
66
 
67
  function extractApiKey(req) {
src/routes/admin.js CHANGED
@@ -32,6 +32,7 @@ const ProxyHelper = require('../utils/proxyHelper')
32
 
33
  const router = express.Router()
34
 
 
35
  function normalizeNullableDate(value) {
36
  if (value === undefined || value === null) {
37
  return null
@@ -43,13 +44,45 @@ function normalizeNullableDate(value) {
43
  return value
44
  }
45
 
46
- function formatSubscriptionExpiry(account) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  if (!account || typeof account !== 'object') {
48
  return account
49
  }
50
 
51
- const rawSubscription = account.subscriptionExpiresAt
52
- const rawToken = account.tokenExpiresAt !== undefined ? account.tokenExpiresAt : account.expiresAt
 
 
 
 
 
53
 
54
  const subscriptionExpiresAt = normalizeNullableDate(rawSubscription)
55
  const tokenExpiresAt = normalizeNullableDate(rawToken)
@@ -2112,7 +2145,6 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
2112
  try {
2113
  const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
2114
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
2115
- const formattedAccount = formatSubscriptionExpiry(account)
2116
 
2117
  // 获取会话窗口使用统计(仅对有活跃窗口的账户)
2118
  let sessionWindowUsage = null
@@ -2154,6 +2186,7 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
2154
  }
2155
  }
2156
 
 
2157
  return {
2158
  ...formattedAccount,
2159
  // 转换schedulable为布尔值
@@ -2171,7 +2204,7 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
2171
  // 如果获取统计失败,返回空统计
2172
  try {
2173
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
2174
- const formattedAccount = formatSubscriptionExpiry(account)
2175
  return {
2176
  ...formattedAccount,
2177
  groupInfos,
@@ -2187,7 +2220,7 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
2187
  `⚠️ Failed to get group info for account ${account.id}:`,
2188
  groupError.message
2189
  )
2190
- const formattedAccount = formatSubscriptionExpiry(account)
2191
  return {
2192
  ...formattedAccount,
2193
  groupInfos: [],
@@ -2203,8 +2236,7 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
2203
  })
2204
  )
2205
 
2206
- const formattedAccounts = accountsWithStats.map(formatSubscriptionExpiry)
2207
- return res.json({ success: true, data: formattedAccounts })
2208
  } catch (error) {
2209
  logger.error('❌ Failed to get Claude accounts:', error)
2210
  return res.status(500).json({ error: 'Failed to get Claude accounts', message: error.message })
@@ -2301,8 +2333,7 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
2301
  useUnifiedUserAgent,
2302
  useUnifiedClientId,
2303
  unifiedClientId,
2304
- expiresAt,
2305
- subscriptionExpiresAt
2306
  } = req.body
2307
 
2308
  if (!name) {
@@ -2346,7 +2377,7 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
2346
  useUnifiedUserAgent: useUnifiedUserAgent === true, // 默认为false
2347
  useUnifiedClientId: useUnifiedClientId === true, // 默认为false
2348
  unifiedClientId: unifiedClientId || '', // 统一的客户端标识
2349
- expiresAt: subscriptionExpiresAt ?? expiresAt ?? null // 账户订阅到期时间
2350
  })
2351
 
2352
  // 如果是分组类型,将账户添加到分组
@@ -2361,8 +2392,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
2361
  }
2362
 
2363
  logger.success(`🏢 Admin created new Claude account: ${name} (${accountType || 'shared'})`)
2364
- const responseAccount = formatSubscriptionExpiry(newAccount)
2365
- return res.json({ success: true, data: responseAccount })
2366
  } catch (error) {
2367
  logger.error('❌ Failed to create Claude account:', error)
2368
  return res
@@ -2377,16 +2408,24 @@ router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) =>
2377
  const { accountId } = req.params
2378
  const updates = req.body
2379
 
 
 
 
2380
  // 验证priority的有效性
2381
  if (
2382
- updates.priority !== undefined &&
2383
- (typeof updates.priority !== 'number' || updates.priority < 1 || updates.priority > 100)
 
 
2384
  ) {
2385
  return res.status(400).json({ error: 'Priority must be a number between 1 and 100' })
2386
  }
2387
 
2388
  // 验证accountType的有效性
2389
- if (updates.accountType && !['shared', 'dedicated', 'group'].includes(updates.accountType)) {
 
 
 
2390
  return res
2391
  .status(400)
2392
  .json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
@@ -2394,9 +2433,9 @@ router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) =>
2394
 
2395
  // 如果更新为分组类型,验证groupId或groupIds
2396
  if (
2397
- updates.accountType === 'group' &&
2398
- !updates.groupId &&
2399
- (!updates.groupIds || updates.groupIds.length === 0)
2400
  ) {
2401
  return res
2402
  .status(400)
@@ -2410,41 +2449,30 @@ router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) =>
2410
  }
2411
 
2412
  // 处理分组的变更
2413
- if (updates.accountType !== undefined) {
2414
  // 如果之前是分组类型,需要从所有分组中移除
2415
  if (currentAccount.accountType === 'group') {
2416
  await accountGroupService.removeAccountFromAllGroups(accountId)
2417
  }
2418
 
2419
  // 如果新类型是分组,添加到新分组
2420
- if (updates.accountType === 'group') {
2421
  // 处理多分组/单分组的兼容性
2422
- if (Object.prototype.hasOwnProperty.call(updates, 'groupIds')) {
2423
- if (updates.groupIds && updates.groupIds.length > 0) {
2424
  // 使用多分组设置
2425
- await accountGroupService.setAccountGroups(accountId, updates.groupIds, 'claude')
2426
  } else {
2427
  // groupIds 为空数组,从所有分组中移除
2428
  await accountGroupService.removeAccountFromAllGroups(accountId)
2429
  }
2430
- } else if (updates.groupId) {
2431
  // 兼容单分组模式
2432
- await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'claude')
2433
  }
2434
  }
2435
  }
2436
 
2437
- // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
2438
- const mappedUpdates = { ...updates }
2439
- if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
2440
- mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt
2441
- } else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) {
2442
- mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
2443
- }
2444
- if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
2445
- delete mappedUpdates.expiresAt
2446
- }
2447
-
2448
  await claudeAccountService.updateAccount(accountId, mappedUpdates)
2449
 
2450
  logger.success(`📝 Admin updated Claude account: ${accountId}`)
@@ -2645,14 +2673,13 @@ router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
2645
  }
2646
 
2647
  // 为每个账户添加使用统计信息
2648
-
2649
  const accountsWithStats = await Promise.all(
2650
  accounts.map(async (account) => {
2651
- const formattedAccount = formatSubscriptionExpiry(account)
2652
  try {
2653
  const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
2654
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
2655
 
 
2656
  return {
2657
  ...formattedAccount,
2658
  // 转换schedulable为布尔值
@@ -2671,6 +2698,7 @@ router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
2671
  )
2672
  try {
2673
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
 
2674
  return {
2675
  ...formattedAccount,
2676
  // 转换schedulable为布尔值
@@ -2687,6 +2715,7 @@ router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
2687
  `⚠️ Failed to get group info for Claude Console account ${account.id}:`,
2688
  groupError.message
2689
  )
 
2690
  return {
2691
  ...formattedAccount,
2692
  groupInfos: [],
@@ -2701,8 +2730,7 @@ router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
2701
  })
2702
  )
2703
 
2704
- const formattedAccounts = accountsWithStats.map(formatSubscriptionExpiry)
2705
- return res.json({ success: true, data: formattedAccounts })
2706
  } catch (error) {
2707
  logger.error('❌ Failed to get Claude Console accounts:', error)
2708
  return res
@@ -2773,8 +2801,8 @@ router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => {
2773
  }
2774
 
2775
  logger.success(`🎮 Admin created Claude Console account: ${name}`)
2776
- const responseAccount = formatSubscriptionExpiry(newAccount)
2777
- return res.json({ success: true, data: responseAccount })
2778
  } catch (error) {
2779
  logger.error('❌ Failed to create Claude Console account:', error)
2780
  return res
@@ -2789,20 +2817,29 @@ router.put('/claude-console-accounts/:accountId', authenticateAdmin, async (req,
2789
  const { accountId } = req.params
2790
  const updates = req.body
2791
 
 
 
 
2792
  // 验证priority的有效性(1-100)
2793
- if (updates.priority !== undefined && (updates.priority < 1 || updates.priority > 100)) {
 
 
 
2794
  return res.status(400).json({ error: 'Priority must be between 1 and 100' })
2795
  }
2796
 
2797
  // 验证accountType的有效性
2798
- if (updates.accountType && !['shared', 'dedicated', 'group'].includes(updates.accountType)) {
 
 
 
2799
  return res
2800
  .status(400)
2801
  .json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
2802
  }
2803
 
2804
  // 如果更新为分组类型,验证groupId
2805
- if (updates.accountType === 'group' && !updates.groupId) {
2806
  return res.status(400).json({ error: 'Group ID is required for group type accounts' })
2807
  }
2808
 
@@ -2813,7 +2850,7 @@ router.put('/claude-console-accounts/:accountId', authenticateAdmin, async (req,
2813
  }
2814
 
2815
  // 处理分组的变更
2816
- if (updates.accountType !== undefined) {
2817
  // 如果之前是分组类型,需要从所有分组中移除
2818
  if (currentAccount.accountType === 'group') {
2819
  const oldGroups = await accountGroupService.getAccountGroups(accountId)
@@ -2822,34 +2859,23 @@ router.put('/claude-console-accounts/:accountId', authenticateAdmin, async (req,
2822
  }
2823
  }
2824
  // 如果新类型是分组,处理多分组支持
2825
- if (updates.accountType === 'group') {
2826
- if (Object.prototype.hasOwnProperty.call(updates, 'groupIds')) {
2827
  // 如果明确提供了 groupIds 参数(包括空数组)
2828
- if (updates.groupIds && updates.groupIds.length > 0) {
2829
  // 设置新的多分组
2830
- await accountGroupService.setAccountGroups(accountId, updates.groupIds, 'claude')
2831
  } else {
2832
  // groupIds 为空数组,从所有分组中移除
2833
  await accountGroupService.removeAccountFromAllGroups(accountId)
2834
  }
2835
- } else if (updates.groupId) {
2836
  // 向后兼容:仅当没有 groupIds 但有 groupId 时使用单分组逻辑
2837
- await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'claude')
2838
  }
2839
  }
2840
  }
2841
 
2842
- // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
2843
- const mappedUpdates = { ...updates }
2844
- if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
2845
- mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt
2846
- } else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) {
2847
- mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
2848
- }
2849
- if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
2850
- delete mappedUpdates.expiresAt
2851
- }
2852
-
2853
  await claudeConsoleAccountService.updateAccount(accountId, mappedUpdates)
2854
 
2855
  logger.success(`📝 Admin updated Claude Console account: ${accountId}`)
@@ -3076,11 +3102,11 @@ router.get('/ccr-accounts', authenticateAdmin, async (req, res) => {
3076
  // 为每个账户添加使用统计信息
3077
  const accountsWithStats = await Promise.all(
3078
  accounts.map(async (account) => {
3079
- const formattedAccount = formatSubscriptionExpiry(account)
3080
  try {
3081
  const usageStats = await redis.getAccountUsageStats(account.id)
3082
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
3083
 
 
3084
  return {
3085
  ...formattedAccount,
3086
  // 转换schedulable为布尔值
@@ -3099,6 +3125,7 @@ router.get('/ccr-accounts', authenticateAdmin, async (req, res) => {
3099
  )
3100
  try {
3101
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
 
3102
  return {
3103
  ...formattedAccount,
3104
  // 转换schedulable为布尔值
@@ -3116,7 +3143,7 @@ router.get('/ccr-accounts', authenticateAdmin, async (req, res) => {
3116
  groupError.message
3117
  )
3118
  return {
3119
- ...formattedAccount,
3120
  groupInfos: [],
3121
  usage: {
3122
  daily: { tokens: 0, requests: 0, allTokens: 0 },
@@ -3129,8 +3156,7 @@ router.get('/ccr-accounts', authenticateAdmin, async (req, res) => {
3129
  })
3130
  )
3131
 
3132
- const formattedAccounts = accountsWithStats.map(formatSubscriptionExpiry)
3133
- return res.json({ success: true, data: formattedAccounts })
3134
  } catch (error) {
3135
  logger.error('❌ Failed to get CCR accounts:', error)
3136
  return res.status(500).json({ error: 'Failed to get CCR accounts', message: error.message })
@@ -3199,8 +3225,8 @@ router.post('/ccr-accounts', authenticateAdmin, async (req, res) => {
3199
  }
3200
 
3201
  logger.success(`🔧 Admin created CCR account: ${name}`)
3202
- const responseAccount = formatSubscriptionExpiry(newAccount)
3203
- return res.json({ success: true, data: responseAccount })
3204
  } catch (error) {
3205
  logger.error('❌ Failed to create CCR account:', error)
3206
  return res.status(500).json({ error: 'Failed to create CCR account', message: error.message })
@@ -3213,20 +3239,29 @@ router.put('/ccr-accounts/:accountId', authenticateAdmin, async (req, res) => {
3213
  const { accountId } = req.params
3214
  const updates = req.body
3215
 
 
 
 
3216
  // 验证priority的有效性(1-100)
3217
- if (updates.priority !== undefined && (updates.priority < 1 || updates.priority > 100)) {
 
 
 
3218
  return res.status(400).json({ error: 'Priority must be between 1 and 100' })
3219
  }
3220
 
3221
  // 验证accountType的有效性
3222
- if (updates.accountType && !['shared', 'dedicated', 'group'].includes(updates.accountType)) {
 
 
 
3223
  return res
3224
  .status(400)
3225
  .json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
3226
  }
3227
 
3228
  // 如果更新为分组类型,验证groupId
3229
- if (updates.accountType === 'group' && !updates.groupId) {
3230
  return res.status(400).json({ error: 'Group ID is required for group type accounts' })
3231
  }
3232
 
@@ -3237,7 +3272,7 @@ router.put('/ccr-accounts/:accountId', authenticateAdmin, async (req, res) => {
3237
  }
3238
 
3239
  // 处理���组的变更
3240
- if (updates.accountType !== undefined) {
3241
  // 如果之前是分组类型,需要从所有分组中移除
3242
  if (currentAccount.accountType === 'group') {
3243
  const oldGroups = await accountGroupService.getAccountGroups(accountId)
@@ -3246,34 +3281,23 @@ router.put('/ccr-accounts/:accountId', authenticateAdmin, async (req, res) => {
3246
  }
3247
  }
3248
  // 如果新类型是分组,处理多分组支持
3249
- if (updates.accountType === 'group') {
3250
- if (Object.prototype.hasOwnProperty.call(updates, 'groupIds')) {
3251
  // 如果明确提供了 groupIds 参数(包括空数组)
3252
- if (updates.groupIds && updates.groupIds.length > 0) {
3253
  // 设置新的多分组
3254
- await accountGroupService.setAccountGroups(accountId, updates.groupIds, 'claude')
3255
  } else {
3256
  // groupIds 为空数组,从所有分组中移除
3257
  await accountGroupService.removeAccountFromAllGroups(accountId)
3258
  }
3259
- } else if (updates.groupId) {
3260
  // 向后兼容:仅当没有 groupIds 但有 groupId 时使用单分组逻辑
3261
- await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'claude')
3262
  }
3263
  }
3264
  }
3265
 
3266
- // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
3267
- const mappedUpdates = { ...updates }
3268
- if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
3269
- mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt
3270
- } else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) {
3271
- mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
3272
- }
3273
- if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
3274
- delete mappedUpdates.expiresAt
3275
- }
3276
-
3277
  await ccrAccountService.updateAccount(accountId, mappedUpdates)
3278
 
3279
  logger.success(`📝 Admin updated CCR account: ${accountId}`)
@@ -3488,11 +3512,11 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
3488
  // 为每个账户添加使用统计信息
3489
  const accountsWithStats = await Promise.all(
3490
  accounts.map(async (account) => {
3491
- const formattedAccount = formatSubscriptionExpiry(account)
3492
  try {
3493
  const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
3494
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
3495
 
 
3496
  return {
3497
  ...formattedAccount,
3498
  groupInfos,
@@ -3509,6 +3533,7 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
3509
  )
3510
  try {
3511
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
 
3512
  return {
3513
  ...formattedAccount,
3514
  groupInfos,
@@ -3524,7 +3549,7 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
3524
  groupError.message
3525
  )
3526
  return {
3527
- ...formattedAccount,
3528
  groupInfos: [],
3529
  usage: {
3530
  daily: { tokens: 0, requests: 0, allTokens: 0 },
@@ -3537,8 +3562,7 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
3537
  })
3538
  )
3539
 
3540
- const formattedAccounts = accountsWithStats.map(formatSubscriptionExpiry)
3541
- return res.json({ success: true, data: formattedAccounts })
3542
  } catch (error) {
3543
  logger.error('❌ Failed to get Bedrock accounts:', error)
3544
  return res.status(500).json({ error: 'Failed to get Bedrock accounts', message: error.message })
@@ -3600,8 +3624,8 @@ router.post('/bedrock-accounts', authenticateAdmin, async (req, res) => {
3600
  }
3601
 
3602
  logger.success(`☁️ Admin created Bedrock account: ${name}`)
3603
- const responseAccount = formatSubscriptionExpiry(result.data)
3604
- return res.json({ success: true, data: responseAccount })
3605
  } catch (error) {
3606
  logger.error('❌ Failed to create Bedrock account:', error)
3607
  return res
@@ -3616,13 +3640,19 @@ router.put('/bedrock-accounts/:accountId', authenticateAdmin, async (req, res) =
3616
  const { accountId } = req.params
3617
  const updates = req.body
3618
 
 
 
 
3619
  // 验证priority的有效性(1-100)
3620
- if (updates.priority !== undefined && (updates.priority < 1 || updates.priority > 100)) {
 
 
 
3621
  return res.status(400).json({ error: 'Priority must be between 1 and 100' })
3622
  }
3623
 
3624
  // 验证accountType的有效性
3625
- if (updates.accountType && !['shared', 'dedicated'].includes(updates.accountType)) {
3626
  return res
3627
  .status(400)
3628
  .json({ error: 'Invalid account type. Must be "shared" or "dedicated"' })
@@ -3630,25 +3660,14 @@ router.put('/bedrock-accounts/:accountId', authenticateAdmin, async (req, res) =
3630
 
3631
  // 验证credentialType的有效性
3632
  if (
3633
- updates.credentialType &&
3634
- !['default', 'access_key', 'bearer_token'].includes(updates.credentialType)
3635
  ) {
3636
  return res.status(400).json({
3637
  error: 'Invalid credential type. Must be "default", "access_key", or "bearer_token"'
3638
  })
3639
  }
3640
 
3641
- // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
3642
- const mappedUpdates = { ...updates }
3643
- if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
3644
- mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt
3645
- } else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) {
3646
- mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
3647
- }
3648
- if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
3649
- delete mappedUpdates.expiresAt
3650
- }
3651
-
3652
  const result = await bedrockAccountService.updateAccount(accountId, mappedUpdates)
3653
 
3654
  if (!result.success) {
@@ -3968,11 +3987,11 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
3968
  // 为每个账户添加使用统计信息(与Claude账户相同的逻辑)
3969
  const accountsWithStats = await Promise.all(
3970
  accounts.map(async (account) => {
3971
- const formattedAccount = formatSubscriptionExpiry(account)
3972
  try {
3973
  const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
3974
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
3975
 
 
3976
  return {
3977
  ...formattedAccount,
3978
  groupInfos,
@@ -3990,6 +4009,7 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
3990
  // 如果获取统计失败,返回空统计
3991
  try {
3992
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
 
3993
  return {
3994
  ...formattedAccount,
3995
  groupInfos,
@@ -4005,7 +4025,7 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
4005
  groupError.message
4006
  )
4007
  return {
4008
- ...formattedAccount,
4009
  groupInfos: [],
4010
  usage: {
4011
  daily: { tokens: 0, requests: 0, allTokens: 0 },
@@ -4018,8 +4038,7 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
4018
  })
4019
  )
4020
 
4021
- const formattedAccounts = accountsWithStats.map(formatSubscriptionExpiry)
4022
- return res.json({ success: true, data: formattedAccounts })
4023
  } catch (error) {
4024
  logger.error('❌ Failed to get Gemini accounts:', error)
4025
  return res.status(500).json({ error: 'Failed to get accounts', message: error.message })
@@ -4059,8 +4078,8 @@ router.post('/gemini-accounts', authenticateAdmin, async (req, res) => {
4059
  }
4060
 
4061
  logger.success(`🏢 Admin created new Gemini account: ${accountData.name}`)
4062
- const responseAccount = formatSubscriptionExpiry(newAccount)
4063
- return res.json({ success: true, data: responseAccount })
4064
  } catch (error) {
4065
  logger.error('❌ Failed to create Gemini account:', error)
4066
  return res.status(500).json({ error: 'Failed to create account', message: error.message })
@@ -4091,8 +4110,11 @@ router.put('/gemini-accounts/:accountId', authenticateAdmin, async (req, res) =>
4091
  return res.status(404).json({ error: 'Account not found' })
4092
  }
4093
 
 
 
 
4094
  // 处理分组的变更
4095
- if (updates.accountType !== undefined) {
4096
  // 如果之前是分组类型,需要从所有分组中移除
4097
  if (currentAccount.accountType === 'group') {
4098
  const oldGroups = await accountGroupService.getAccountGroups(accountId)
@@ -4101,39 +4123,27 @@ router.put('/gemini-accounts/:accountId', authenticateAdmin, async (req, res) =>
4101
  }
4102
  }
4103
  // 如果新类型是分组,处理多分组支持
4104
- if (updates.accountType === 'group') {
4105
- if (Object.prototype.hasOwnProperty.call(updates, 'groupIds')) {
4106
  // 如果明确提供了 groupIds 参数(包括空数组)
4107
- if (updates.groupIds && updates.groupIds.length > 0) {
4108
  // 设置新的多分组
4109
- await accountGroupService.setAccountGroups(accountId, updates.groupIds, 'gemini')
4110
  } else {
4111
  // groupIds 为空数组,从所有分组中移除
4112
  await accountGroupService.removeAccountFromAllGroups(accountId)
4113
  }
4114
- } else if (updates.groupId) {
4115
  // 向后兼容:仅当没有 groupIds 但有 groupId 时使用单分组逻辑
4116
- await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'gemini')
4117
  }
4118
  }
4119
  }
4120
 
4121
- // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
4122
- const mappedUpdates = { ...updates }
4123
- if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
4124
- mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt
4125
- } else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) {
4126
- mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
4127
- }
4128
- if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
4129
- delete mappedUpdates.expiresAt
4130
- }
4131
-
4132
  const updatedAccount = await geminiAccountService.updateAccount(accountId, mappedUpdates)
4133
 
4134
  logger.success(`📝 Admin updated Gemini account: ${accountId}`)
4135
- const responseAccount = formatSubscriptionExpiry(updatedAccount)
4136
- return res.json({ success: true, data: responseAccount })
4137
  } catch (error) {
4138
  logger.error('❌ Failed to update Gemini account:', error)
4139
  return res.status(500).json({ error: 'Failed to update account', message: error.message })
@@ -7281,7 +7291,7 @@ router.get('/openai-accounts', authenticateAdmin, async (req, res) => {
7281
  try {
7282
  const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
7283
  const groupInfos = await fetchAccountGroups(account.id)
7284
- const formattedAccount = formatSubscriptionExpiry(account)
7285
  return {
7286
  ...formattedAccount,
7287
  groupInfos,
@@ -7294,7 +7304,7 @@ router.get('/openai-accounts', authenticateAdmin, async (req, res) => {
7294
  } catch (error) {
7295
  logger.debug(`Failed to get usage stats for OpenAI account ${account.id}:`, error)
7296
  const groupInfos = await fetchAccountGroups(account.id)
7297
- const formattedAccount = formatSubscriptionExpiry(account)
7298
  return {
7299
  ...formattedAccount,
7300
  groupInfos,
@@ -7310,11 +7320,9 @@ router.get('/openai-accounts', authenticateAdmin, async (req, res) => {
7310
 
7311
  logger.info(`获取 OpenAI 账户列表: ${accountsWithStats.length} 个账户`)
7312
 
7313
- const formattedAccounts = accountsWithStats.map(formatSubscriptionExpiry)
7314
-
7315
  return res.json({
7316
  success: true,
7317
- data: formattedAccounts
7318
  })
7319
  } catch (error) {
7320
  logger.error('获取 OpenAI 账户列表失败:', error)
@@ -7340,8 +7348,7 @@ router.post('/openai-accounts', authenticateAdmin, async (req, res) => {
7340
  rateLimitDuration,
7341
  priority,
7342
  needsImmediateRefresh, // 是否需要立即刷新
7343
- requireRefreshSuccess, // 是否必须刷新成功才能创建
7344
- subscriptionExpiresAt
7345
  } = req.body
7346
 
7347
  if (!name) {
@@ -7363,8 +7370,7 @@ router.post('/openai-accounts', authenticateAdmin, async (req, res) => {
7363
  accountInfo: accountInfo || {},
7364
  proxy: proxy || null,
7365
  isActive: true,
7366
- schedulable: true,
7367
- subscriptionExpiresAt: subscriptionExpiresAt || null
7368
  }
7369
 
7370
  // 如果需要立即刷新且必须成功(OpenAI 手动模式)
@@ -7400,11 +7406,9 @@ router.post('/openai-accounts', authenticateAdmin, async (req, res) => {
7400
 
7401
  logger.success(`✅ ��建并验证 OpenAI 账户成功: ${name} (ID: ${tempAccount.id})`)
7402
 
7403
- const responseAccount = formatSubscriptionExpiry(refreshedAccount)
7404
-
7405
  return res.json({
7406
  success: true,
7407
- data: responseAccount,
7408
  message: '账户创建成功,并已获取完整 token 信息'
7409
  })
7410
  } catch (refreshError) {
@@ -7466,11 +7470,9 @@ router.post('/openai-accounts', authenticateAdmin, async (req, res) => {
7466
 
7467
  logger.success(`✅ 创建 OpenAI 账户成功: ${name} (ID: ${createdAccount.id})`)
7468
 
7469
- const responseAccount = formatSubscriptionExpiry(createdAccount)
7470
-
7471
  return res.json({
7472
  success: true,
7473
- data: responseAccount
7474
  })
7475
  } catch (error) {
7476
  logger.error('创建 OpenAI 账户失败:', error)
@@ -7487,17 +7489,24 @@ router.put('/openai-accounts/:id', authenticateAdmin, async (req, res) => {
7487
  try {
7488
  const { id } = req.params
7489
  const updates = req.body
7490
- const { needsImmediateRefresh, requireRefreshSuccess } = updates
 
 
 
 
7491
 
7492
  // 验证accountType的有效性
7493
- if (updates.accountType && !['shared', 'dedicated', 'group'].includes(updates.accountType)) {
 
 
 
7494
  return res
7495
  .status(400)
7496
  .json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
7497
  }
7498
 
7499
  // 如果更新为分组类型,验证groupId
7500
- if (updates.accountType === 'group' && !updates.groupId) {
7501
  return res.status(400).json({ error: 'Group ID is required for group type accounts' })
7502
  }
7503
 
@@ -7508,18 +7517,18 @@ router.put('/openai-accounts/:id', authenticateAdmin, async (req, res) => {
7508
  }
7509
 
7510
  // 如果更新了 Refresh Token,需要验证其有效性
7511
- if (updates.openaiOauth?.refreshToken && needsImmediateRefresh && requireRefreshSuccess) {
7512
  // 先更新 token 信息
7513
  const tempUpdateData = {}
7514
- if (updates.openaiOauth.refreshToken) {
7515
- tempUpdateData.refreshToken = updates.openaiOauth.refreshToken
7516
  }
7517
- if (updates.openaiOauth.accessToken) {
7518
- tempUpdateData.accessToken = updates.openaiOauth.accessToken
7519
  }
7520
  // 更新代理配置(如果有)
7521
- if (updates.proxy !== undefined) {
7522
- tempUpdateData.proxy = updates.proxy
7523
  }
7524
 
7525
  // 临时更新账户以测试新的 token
@@ -7595,7 +7604,7 @@ router.put('/openai-accounts/:id', authenticateAdmin, async (req, res) => {
7595
  }
7596
 
7597
  // 处理分组的变更
7598
- if (updates.accountType !== undefined) {
7599
  // 如果之前是分组类型,需要从原分组中移除
7600
  if (currentAccount.accountType === 'group') {
7601
  const oldGroup = await accountGroupService.getAccountGroup(id)
@@ -7604,65 +7613,50 @@ router.put('/openai-accounts/:id', authenticateAdmin, async (req, res) => {
7604
  }
7605
  }
7606
  // 如果新类型是分组,添加到新分组
7607
- if (updates.accountType === 'group' && updates.groupId) {
7608
- await accountGroupService.addAccountToGroup(id, updates.groupId, 'openai')
7609
  }
7610
  }
7611
 
7612
  // 准备更新数据
7613
- const updateData = { ...updates }
7614
 
7615
  // 处理敏感数据加密
7616
- if (updates.openaiOauth) {
7617
- updateData.openaiOauth = updates.openaiOauth
7618
  // 编辑时不允许直接输入 ID Token,只能通过刷新获取
7619
- if (updates.openaiOauth.accessToken) {
7620
- updateData.accessToken = updates.openaiOauth.accessToken
7621
  }
7622
- if (updates.openaiOauth.refreshToken) {
7623
- updateData.refreshToken = updates.openaiOauth.refreshToken
7624
  }
7625
- if (updates.openaiOauth.expires_in) {
7626
  updateData.expiresAt = new Date(
7627
- Date.now() + updates.openaiOauth.expires_in * 1000
7628
  ).toISOString()
7629
  }
7630
  }
7631
 
7632
  // 更新账户信息
7633
- if (updates.accountInfo) {
7634
- updateData.accountId = updates.accountInfo.accountId || currentAccount.accountId
7635
- updateData.chatgptUserId = updates.accountInfo.chatgptUserId || currentAccount.chatgptUserId
 
7636
  updateData.organizationId =
7637
- updates.accountInfo.organizationId || currentAccount.organizationId
7638
  updateData.organizationRole =
7639
- updates.accountInfo.organizationRole || currentAccount.organizationRole
7640
  updateData.organizationTitle =
7641
- updates.accountInfo.organizationTitle || currentAccount.organizationTitle
7642
- updateData.planType = updates.accountInfo.planType || currentAccount.planType
7643
- updateData.email = updates.accountInfo.email || currentAccount.email
7644
  updateData.emailVerified =
7645
- updates.accountInfo.emailVerified !== undefined
7646
- ? updates.accountInfo.emailVerified
7647
  : currentAccount.emailVerified
7648
  }
7649
 
7650
- const hasOauthExpiry = Boolean(updates.openaiOauth?.expires_in)
7651
-
7652
- // 处理订阅过期时间字段:优先使用 subscriptionExpiresAt,兼容旧版的 expiresAt
7653
- if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
7654
- updateData.subscriptionExpiresAt = updates.subscriptionExpiresAt
7655
- } else if (Object.prototype.hasOwnProperty.call(updates, 'expiresAt') && !hasOauthExpiry) {
7656
- updateData.subscriptionExpiresAt = updates.expiresAt
7657
- }
7658
-
7659
- if (
7660
- !hasOauthExpiry &&
7661
- Object.prototype.hasOwnProperty.call(updateData, 'subscriptionExpiresAt')
7662
- ) {
7663
- delete updateData.expiresAt
7664
- }
7665
-
7666
  const updatedAccount = await openaiAccountService.updateAccount(id, updateData)
7667
 
7668
  // 如果需要刷新但不强制成功(非关键更新)
@@ -7677,8 +7671,7 @@ router.put('/openai-accounts/:id', authenticateAdmin, async (req, res) => {
7677
  }
7678
 
7679
  logger.success(`📝 Admin updated OpenAI account: ${id}`)
7680
- const responseAccount = formatSubscriptionExpiry(updatedAccount)
7681
- return res.json({ success: true, data: responseAccount })
7682
  } catch (error) {
7683
  logger.error('❌ Failed to update OpenAI account:', error)
7684
  return res.status(500).json({ error: 'Failed to update account', message: error.message })
@@ -7759,11 +7752,9 @@ router.put('/openai-accounts/:id/toggle', authenticateAdmin, async (req, res) =>
7759
  `✅ ${account.enabled ? '启用' : '禁用'} OpenAI 账户: ${account.name} (ID: ${id})`
7760
  )
7761
 
7762
- const responseAccount = formatSubscriptionExpiry(account)
7763
-
7764
  return res.json({
7765
  success: true,
7766
- data: responseAccount
7767
  })
7768
  } catch (error) {
7769
  logger.error('切换 OpenAI 账户状态失败:', error)
@@ -7869,10 +7860,10 @@ router.get('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
7869
  // 为每个账户添加使用统计信息和分组信息
7870
  const accountsWithStats = await Promise.all(
7871
  accounts.map(async (account) => {
7872
- const formattedAccount = formatSubscriptionExpiry(account)
7873
  try {
7874
  const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
7875
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
 
7876
  return {
7877
  ...formattedAccount,
7878
  groupInfos,
@@ -7886,6 +7877,7 @@ router.get('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
7886
  logger.debug(`Failed to get usage stats for Azure OpenAI account ${account.id}:`, error)
7887
  try {
7888
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
 
7889
  return {
7890
  ...formattedAccount,
7891
  groupInfos,
@@ -7898,7 +7890,7 @@ router.get('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
7898
  } catch (groupError) {
7899
  logger.debug(`Failed to get group info for account ${account.id}:`, groupError)
7900
  return {
7901
- ...formattedAccount,
7902
  groupInfos: [],
7903
  usage: {
7904
  daily: { requests: 0, tokens: 0, allTokens: 0 },
@@ -7911,11 +7903,9 @@ router.get('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
7911
  })
7912
  )
7913
 
7914
- const formattedAccounts = accountsWithStats.map(formatSubscriptionExpiry)
7915
-
7916
  res.json({
7917
  success: true,
7918
- data: formattedAccounts
7919
  })
7920
  } catch (error) {
7921
  logger.error('Failed to fetch Azure OpenAI accounts:', error)
@@ -8034,11 +8024,9 @@ router.post('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
8034
  }
8035
  }
8036
 
8037
- const responseAccount = formatSubscriptionExpiry(account)
8038
-
8039
  res.json({
8040
  success: true,
8041
- data: responseAccount,
8042
  message: 'Azure OpenAI account created successfully'
8043
  })
8044
  } catch (error) {
@@ -8057,23 +8045,14 @@ router.put('/azure-openai-accounts/:id', authenticateAdmin, async (req, res) =>
8057
  const { id } = req.params
8058
  const updates = req.body
8059
 
8060
- // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
8061
- const mappedUpdates = { ...updates }
8062
- if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
8063
- mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt
8064
- } else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) {
8065
- mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
8066
- }
8067
- if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
8068
- delete mappedUpdates.expiresAt
8069
- }
8070
 
8071
  const account = await azureOpenaiAccountService.updateAccount(id, mappedUpdates)
8072
- const responseAccount = formatSubscriptionExpiry(account)
8073
 
8074
  res.json({
8075
  success: true,
8076
- data: responseAccount,
8077
  message: 'Azure OpenAI account updated successfully'
8078
  })
8079
  } catch (error) {
@@ -8326,7 +8305,6 @@ router.get('/openai-responses-accounts', authenticateAdmin, async (req, res) =>
8326
  // 处理额度信息、使用统计和绑定的 API Key 数量
8327
  const accountsWithStats = await Promise.all(
8328
  accounts.map(async (account) => {
8329
- const formattedAccount = formatSubscriptionExpiry(account)
8330
  try {
8331
  // 检查是否需要重置额度
8332
  const today = redis.getDateStringInTimezone()
@@ -8380,6 +8358,7 @@ router.get('/openai-responses-accounts', authenticateAdmin, async (req, res) =>
8380
  logger.info(`OpenAI-Responses account ${account.id} has ${boundCount} bound API keys`)
8381
  }
8382
 
 
8383
  return {
8384
  ...formattedAccount,
8385
  boundApiKeysCount: boundCount,
@@ -8391,6 +8370,7 @@ router.get('/openai-responses-accounts', authenticateAdmin, async (req, res) =>
8391
  }
8392
  } catch (error) {
8393
  logger.error(`Failed to process OpenAI-Responses account ${account.id}:`, error)
 
8394
  return {
8395
  ...formattedAccount,
8396
  boundApiKeysCount: 0,
@@ -8404,9 +8384,7 @@ router.get('/openai-responses-accounts', authenticateAdmin, async (req, res) =>
8404
  })
8405
  )
8406
 
8407
- const formattedAccounts = accountsWithStats.map(formatSubscriptionExpiry)
8408
-
8409
- res.json({ success: true, data: formattedAccounts })
8410
  } catch (error) {
8411
  logger.error('Failed to get OpenAI-Responses accounts:', error)
8412
  res.status(500).json({ success: false, message: error.message })
@@ -8417,8 +8395,8 @@ router.get('/openai-responses-accounts', authenticateAdmin, async (req, res) =>
8417
  router.post('/openai-responses-accounts', authenticateAdmin, async (req, res) => {
8418
  try {
8419
  const account = await openaiResponsesAccountService.createAccount(req.body)
8420
- const responseAccount = formatSubscriptionExpiry(account)
8421
- res.json({ success: true, data: responseAccount })
8422
  } catch (error) {
8423
  logger.error('Failed to create OpenAI-Responses account:', error)
8424
  res.status(500).json({
@@ -8434,27 +8412,19 @@ router.put('/openai-responses-accounts/:id', authenticateAdmin, async (req, res)
8434
  const { id } = req.params
8435
  const updates = req.body
8436
 
 
 
 
8437
  // 验证priority的有效性(1-100)
8438
- if (updates.priority !== undefined) {
8439
- const priority = parseInt(updates.priority)
8440
  if (isNaN(priority) || priority < 1 || priority > 100) {
8441
  return res.status(400).json({
8442
  success: false,
8443
  message: 'Priority must be a number between 1 and 100'
8444
  })
8445
  }
8446
- updates.priority = priority.toString()
8447
- }
8448
-
8449
- // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
8450
- const mappedUpdates = { ...updates }
8451
- if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
8452
- mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt
8453
- } else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) {
8454
- mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
8455
- }
8456
- if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
8457
- delete mappedUpdates.expiresAt
8458
  }
8459
 
8460
  const result = await openaiResponsesAccountService.updateAccount(id, mappedUpdates)
@@ -8463,13 +8433,7 @@ router.put('/openai-responses-accounts/:id', authenticateAdmin, async (req, res)
8463
  return res.status(400).json(result)
8464
  }
8465
 
8466
- const updatedAccountData = await openaiResponsesAccountService.getAccount(id)
8467
- if (updatedAccountData) {
8468
- updatedAccountData.apiKey = '***'
8469
- }
8470
- const responseAccount = formatSubscriptionExpiry(updatedAccountData)
8471
-
8472
- res.json({ success: true, data: responseAccount })
8473
  } catch (error) {
8474
  logger.error('Failed to update OpenAI-Responses account:', error)
8475
  res.status(500).json({
@@ -8799,7 +8763,6 @@ router.get('/droid-accounts', authenticateAdmin, async (req, res) => {
8799
  // 添加使用统计
8800
  const accountsWithStats = await Promise.all(
8801
  accounts.map(async (account) => {
8802
- const formattedAccount = formatSubscriptionExpiry(account)
8803
  try {
8804
  const usageStats = await redis.getAccountUsageStats(account.id, 'droid')
8805
  let groupInfos = []
@@ -8828,6 +8791,7 @@ router.get('/droid-accounts', authenticateAdmin, async (req, res) => {
8828
  return count
8829
  }, 0)
8830
 
 
8831
  return {
8832
  ...formattedAccount,
8833
  schedulable: account.schedulable === 'true',
@@ -8841,6 +8805,7 @@ router.get('/droid-accounts', authenticateAdmin, async (req, res) => {
8841
  }
8842
  } catch (error) {
8843
  logger.warn(`Failed to get stats for Droid account ${account.id}:`, error.message)
 
8844
  return {
8845
  ...formattedAccount,
8846
  boundApiKeysCount: 0,
@@ -8855,9 +8820,7 @@ router.get('/droid-accounts', authenticateAdmin, async (req, res) => {
8855
  })
8856
  )
8857
 
8858
- const formattedAccounts = accountsWithStats.map(formatSubscriptionExpiry)
8859
-
8860
- return res.json({ success: true, data: formattedAccounts })
8861
  } catch (error) {
8862
  logger.error('Failed to get Droid accounts:', error)
8863
  return res.status(500).json({ error: 'Failed to get Droid accounts', message: error.message })
@@ -8914,8 +8877,8 @@ router.post('/droid-accounts', authenticateAdmin, async (req, res) => {
8914
  }
8915
 
8916
  logger.success(`Created Droid account: ${account.name} (${account.id})`)
8917
- const responseAccount = formatSubscriptionExpiry(account)
8918
- return res.json({ success: true, data: responseAccount })
8919
  } catch (error) {
8920
  logger.error('Failed to create Droid account:', error)
8921
  return res.status(500).json({ error: 'Failed to create Droid account', message: error.message })
@@ -8927,7 +8890,11 @@ router.put('/droid-accounts/:id', authenticateAdmin, async (req, res) => {
8927
  try {
8928
  const { id } = req.params
8929
  const updates = { ...req.body }
8930
- const { accountType: rawAccountType, groupId, groupIds } = updates
 
 
 
 
8931
 
8932
  if (rawAccountType && !['shared', 'dedicated', 'group'].includes(rawAccountType)) {
8933
  return res.status(400).json({ error: '账户类型必须是 shared、dedicated 或 group' })
@@ -8949,26 +8916,15 @@ router.put('/droid-accounts/:id', authenticateAdmin, async (req, res) => {
8949
  const normalizedGroupIds = Array.isArray(groupIds)
8950
  ? groupIds.filter((gid) => typeof gid === 'string' && gid.trim())
8951
  : []
8952
- const hasGroupIdsField = Object.prototype.hasOwnProperty.call(updates, 'groupIds')
8953
- const hasGroupIdField = Object.prototype.hasOwnProperty.call(updates, 'groupId')
8954
  const targetAccountType = rawAccountType || currentAccount.accountType || 'shared'
8955
 
8956
- delete updates.groupId
8957
- delete updates.groupIds
8958
 
8959
  if (rawAccountType) {
8960
- updates.accountType = targetAccountType
8961
- }
8962
-
8963
- // 映射字段名:前端的expiresAt -> 后端的subscriptionExpiresAt
8964
- const mappedUpdates = { ...updates }
8965
- if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
8966
- mappedUpdates.subscriptionExpiresAt = updates.subscriptionExpiresAt
8967
- } else if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'expiresAt')) {
8968
- mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
8969
- }
8970
- if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'subscriptionExpiresAt')) {
8971
- delete mappedUpdates.expiresAt
8972
  }
8973
 
8974
  const account = await droidAccountService.updateAccount(id, mappedUpdates)
@@ -9003,8 +8959,7 @@ router.put('/droid-accounts/:id', authenticateAdmin, async (req, res) => {
9003
  }
9004
  }
9005
 
9006
- const responseAccount = formatSubscriptionExpiry(account)
9007
- return res.json({ success: true, data: responseAccount })
9008
  } catch (error) {
9009
  logger.error(`Failed to update Droid account ${req.params.id}:`, error)
9010
  return res.status(500).json({ error: 'Failed to update Droid account', message: error.message })
 
32
 
33
  const router = express.Router()
34
 
35
+ // 🛠️ 工具函数:处理可为空的时间字段
36
  function normalizeNullableDate(value) {
37
  if (value === undefined || value === null) {
38
  return null
 
44
  return value
45
  }
46
 
47
+ // 🛠️ 工具函数:映射前端字段名到后端字段名
48
+ /**
49
+ * 映射前端的 expiresAt 字段到后端的 subscriptionExpiresAt 字段
50
+ * @param {Object} updates - 更新对象
51
+ * @param {string} accountType - 账户类型 (如 'Claude', 'OpenAI' 等)
52
+ * @param {string} accountId - 账户 ID
53
+ * @returns {Object} 映射后的更新对象
54
+ */
55
+ function mapExpiryField(updates, accountType, accountId) {
56
+ const mappedUpdates = { ...updates }
57
+ if ('expiresAt' in mappedUpdates) {
58
+ mappedUpdates.subscriptionExpiresAt = mappedUpdates.expiresAt
59
+ delete mappedUpdates.expiresAt
60
+ logger.info(
61
+ `Mapping expiresAt to subscriptionExpiresAt for ${accountType} account ${accountId}`
62
+ )
63
+ }
64
+ return mappedUpdates
65
+ }
66
+
67
+ /**
68
+ * 格式化账户数据,确保前端获取正确的过期时间字段
69
+ * 将 subscriptionExpiresAt(订阅过期时间)映射到 expiresAt 供前端使用
70
+ * 保留原始的 tokenExpiresAt(OAuth token过期时间)供内部使用
71
+ * @param {Object} account - 账户对象
72
+ * @returns {Object} 格式化后的账户对象
73
+ */
74
+ function formatAccountExpiry(account) {
75
  if (!account || typeof account !== 'object') {
76
  return account
77
  }
78
 
79
+ const rawSubscription = Object.prototype.hasOwnProperty.call(account, 'subscriptionExpiresAt')
80
+ ? account.subscriptionExpiresAt
81
+ : null
82
+
83
+ const rawToken = Object.prototype.hasOwnProperty.call(account, 'tokenExpiresAt')
84
+ ? account.tokenExpiresAt
85
+ : account.expiresAt
86
 
87
  const subscriptionExpiresAt = normalizeNullableDate(rawSubscription)
88
  const tokenExpiresAt = normalizeNullableDate(rawToken)
 
2145
  try {
2146
  const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
2147
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
 
2148
 
2149
  // 获取会话窗口使用统计(仅对有活跃窗口的账户)
2150
  let sessionWindowUsage = null
 
2186
  }
2187
  }
2188
 
2189
+ const formattedAccount = formatAccountExpiry(account)
2190
  return {
2191
  ...formattedAccount,
2192
  // 转换schedulable为布尔值
 
2204
  // 如果获取统计失败,返回空统计
2205
  try {
2206
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
2207
+ const formattedAccount = formatAccountExpiry(account)
2208
  return {
2209
  ...formattedAccount,
2210
  groupInfos,
 
2220
  `⚠️ Failed to get group info for account ${account.id}:`,
2221
  groupError.message
2222
  )
2223
+ const formattedAccount = formatAccountExpiry(account)
2224
  return {
2225
  ...formattedAccount,
2226
  groupInfos: [],
 
2236
  })
2237
  )
2238
 
2239
+ return res.json({ success: true, data: accountsWithStats })
 
2240
  } catch (error) {
2241
  logger.error('❌ Failed to get Claude accounts:', error)
2242
  return res.status(500).json({ error: 'Failed to get Claude accounts', message: error.message })
 
2333
  useUnifiedUserAgent,
2334
  useUnifiedClientId,
2335
  unifiedClientId,
2336
+ expiresAt
 
2337
  } = req.body
2338
 
2339
  if (!name) {
 
2377
  useUnifiedUserAgent: useUnifiedUserAgent === true, // 默认为false
2378
  useUnifiedClientId: useUnifiedClientId === true, // 默认为false
2379
  unifiedClientId: unifiedClientId || '', // 统一的客户端标识
2380
+ expiresAt: expiresAt || null // 账���订阅到期时间
2381
  })
2382
 
2383
  // 如果是分组类型,将账户添加到分组
 
2392
  }
2393
 
2394
  logger.success(`🏢 Admin created new Claude account: ${name} (${accountType || 'shared'})`)
2395
+ const formattedAccount = formatAccountExpiry(newAccount)
2396
+ return res.json({ success: true, data: formattedAccount })
2397
  } catch (error) {
2398
  logger.error('❌ Failed to create Claude account:', error)
2399
  return res
 
2408
  const { accountId } = req.params
2409
  const updates = req.body
2410
 
2411
+ // ✅ 【修改】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt(提前到参数验证之前)
2412
+ const mappedUpdates = mapExpiryField(updates, 'Claude', accountId)
2413
+
2414
  // 验证priority的有效性
2415
  if (
2416
+ mappedUpdates.priority !== undefined &&
2417
+ (typeof mappedUpdates.priority !== 'number' ||
2418
+ mappedUpdates.priority < 1 ||
2419
+ mappedUpdates.priority > 100)
2420
  ) {
2421
  return res.status(400).json({ error: 'Priority must be a number between 1 and 100' })
2422
  }
2423
 
2424
  // 验证accountType的有效性
2425
+ if (
2426
+ mappedUpdates.accountType &&
2427
+ !['shared', 'dedicated', 'group'].includes(mappedUpdates.accountType)
2428
+ ) {
2429
  return res
2430
  .status(400)
2431
  .json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
 
2433
 
2434
  // 如果更新为分组类型,验证groupId或groupIds
2435
  if (
2436
+ mappedUpdates.accountType === 'group' &&
2437
+ !mappedUpdates.groupId &&
2438
+ (!mappedUpdates.groupIds || mappedUpdates.groupIds.length === 0)
2439
  ) {
2440
  return res
2441
  .status(400)
 
2449
  }
2450
 
2451
  // 处理分组的变更
2452
+ if (mappedUpdates.accountType !== undefined) {
2453
  // 如果之前是分组类型,需要从所有分组中移除
2454
  if (currentAccount.accountType === 'group') {
2455
  await accountGroupService.removeAccountFromAllGroups(accountId)
2456
  }
2457
 
2458
  // 如果新类型是分组,添加到新分组
2459
+ if (mappedUpdates.accountType === 'group') {
2460
  // 处理多分组/单分组的兼容性
2461
+ if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'groupIds')) {
2462
+ if (mappedUpdates.groupIds && mappedUpdates.groupIds.length > 0) {
2463
  // 使用多分组设置
2464
+ await accountGroupService.setAccountGroups(accountId, mappedUpdates.groupIds, 'claude')
2465
  } else {
2466
  // groupIds 为空数组,从所有分组中移除
2467
  await accountGroupService.removeAccountFromAllGroups(accountId)
2468
  }
2469
+ } else if (mappedUpdates.groupId) {
2470
  // 兼容单分组模式
2471
+ await accountGroupService.addAccountToGroup(accountId, mappedUpdates.groupId, 'claude')
2472
  }
2473
  }
2474
  }
2475
 
 
 
 
 
 
 
 
 
 
 
 
2476
  await claudeAccountService.updateAccount(accountId, mappedUpdates)
2477
 
2478
  logger.success(`📝 Admin updated Claude account: ${accountId}`)
 
2673
  }
2674
 
2675
  // 为每个账户添加使用统计信息
 
2676
  const accountsWithStats = await Promise.all(
2677
  accounts.map(async (account) => {
 
2678
  try {
2679
  const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
2680
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
2681
 
2682
+ const formattedAccount = formatAccountExpiry(account)
2683
  return {
2684
  ...formattedAccount,
2685
  // 转换schedulable为布尔值
 
2698
  )
2699
  try {
2700
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
2701
+ const formattedAccount = formatAccountExpiry(account)
2702
  return {
2703
  ...formattedAccount,
2704
  // 转换schedulable为布尔值
 
2715
  `⚠️ Failed to get group info for Claude Console account ${account.id}:`,
2716
  groupError.message
2717
  )
2718
+ const formattedAccount = formatAccountExpiry(account)
2719
  return {
2720
  ...formattedAccount,
2721
  groupInfos: [],
 
2730
  })
2731
  )
2732
 
2733
+ return res.json({ success: true, data: accountsWithStats })
 
2734
  } catch (error) {
2735
  logger.error('❌ Failed to get Claude Console accounts:', error)
2736
  return res
 
2801
  }
2802
 
2803
  logger.success(`🎮 Admin created Claude Console account: ${name}`)
2804
+ const formattedAccount = formatAccountExpiry(newAccount)
2805
+ return res.json({ success: true, data: formattedAccount })
2806
  } catch (error) {
2807
  logger.error('❌ Failed to create Claude Console account:', error)
2808
  return res
 
2817
  const { accountId } = req.params
2818
  const updates = req.body
2819
 
2820
+ // ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
2821
+ const mappedUpdates = mapExpiryField(updates, 'Claude Console', accountId)
2822
+
2823
  // 验证priority的有效性(1-100)
2824
+ if (
2825
+ mappedUpdates.priority !== undefined &&
2826
+ (mappedUpdates.priority < 1 || mappedUpdates.priority > 100)
2827
+ ) {
2828
  return res.status(400).json({ error: 'Priority must be between 1 and 100' })
2829
  }
2830
 
2831
  // 验证accountType的有效性
2832
+ if (
2833
+ mappedUpdates.accountType &&
2834
+ !['shared', 'dedicated', 'group'].includes(mappedUpdates.accountType)
2835
+ ) {
2836
  return res
2837
  .status(400)
2838
  .json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
2839
  }
2840
 
2841
  // 如果更新为分组类型,验证groupId
2842
+ if (mappedUpdates.accountType === 'group' && !mappedUpdates.groupId) {
2843
  return res.status(400).json({ error: 'Group ID is required for group type accounts' })
2844
  }
2845
 
 
2850
  }
2851
 
2852
  // 处理分组的变更
2853
+ if (mappedUpdates.accountType !== undefined) {
2854
  // 如果之前是分组类型,需要从所有分组中移除
2855
  if (currentAccount.accountType === 'group') {
2856
  const oldGroups = await accountGroupService.getAccountGroups(accountId)
 
2859
  }
2860
  }
2861
  // 如果新类型是分组,处理多分组支持
2862
+ if (mappedUpdates.accountType === 'group') {
2863
+ if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'groupIds')) {
2864
  // 如果明确提供了 groupIds 参数(包括空数组)
2865
+ if (mappedUpdates.groupIds && mappedUpdates.groupIds.length > 0) {
2866
  // 设置新的多分组
2867
+ await accountGroupService.setAccountGroups(accountId, mappedUpdates.groupIds, 'claude')
2868
  } else {
2869
  // groupIds 为空数组,从所有分组中移除
2870
  await accountGroupService.removeAccountFromAllGroups(accountId)
2871
  }
2872
+ } else if (mappedUpdates.groupId) {
2873
  // 向后兼容:仅当没有 groupIds 但有 groupId 时使用单分组逻辑
2874
+ await accountGroupService.addAccountToGroup(accountId, mappedUpdates.groupId, 'claude')
2875
  }
2876
  }
2877
  }
2878
 
 
 
 
 
 
 
 
 
 
 
 
2879
  await claudeConsoleAccountService.updateAccount(accountId, mappedUpdates)
2880
 
2881
  logger.success(`📝 Admin updated Claude Console account: ${accountId}`)
 
3102
  // 为每个账户添加使用统计信息
3103
  const accountsWithStats = await Promise.all(
3104
  accounts.map(async (account) => {
 
3105
  try {
3106
  const usageStats = await redis.getAccountUsageStats(account.id)
3107
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
3108
 
3109
+ const formattedAccount = formatAccountExpiry(account)
3110
  return {
3111
  ...formattedAccount,
3112
  // 转换schedulable为布尔值
 
3125
  )
3126
  try {
3127
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
3128
+ const formattedAccount = formatAccountExpiry(account)
3129
  return {
3130
  ...formattedAccount,
3131
  // 转换schedulable为布尔值
 
3143
  groupError.message
3144
  )
3145
  return {
3146
+ ...account,
3147
  groupInfos: [],
3148
  usage: {
3149
  daily: { tokens: 0, requests: 0, allTokens: 0 },
 
3156
  })
3157
  )
3158
 
3159
+ return res.json({ success: true, data: accountsWithStats })
 
3160
  } catch (error) {
3161
  logger.error('❌ Failed to get CCR accounts:', error)
3162
  return res.status(500).json({ error: 'Failed to get CCR accounts', message: error.message })
 
3225
  }
3226
 
3227
  logger.success(`🔧 Admin created CCR account: ${name}`)
3228
+ const formattedAccount = formatAccountExpiry(newAccount)
3229
+ return res.json({ success: true, data: formattedAccount })
3230
  } catch (error) {
3231
  logger.error('❌ Failed to create CCR account:', error)
3232
  return res.status(500).json({ error: 'Failed to create CCR account', message: error.message })
 
3239
  const { accountId } = req.params
3240
  const updates = req.body
3241
 
3242
+ // ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
3243
+ const mappedUpdates = mapExpiryField(updates, 'CCR', accountId)
3244
+
3245
  // 验证priority的有效性(1-100)
3246
+ if (
3247
+ mappedUpdates.priority !== undefined &&
3248
+ (mappedUpdates.priority < 1 || mappedUpdates.priority > 100)
3249
+ ) {
3250
  return res.status(400).json({ error: 'Priority must be between 1 and 100' })
3251
  }
3252
 
3253
  // 验证accountType的有效性
3254
+ if (
3255
+ mappedUpdates.accountType &&
3256
+ !['shared', 'dedicated', 'group'].includes(mappedUpdates.accountType)
3257
+ ) {
3258
  return res
3259
  .status(400)
3260
  .json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
3261
  }
3262
 
3263
  // 如果更新为分组类型,验证groupId
3264
+ if (mappedUpdates.accountType === 'group' && !mappedUpdates.groupId) {
3265
  return res.status(400).json({ error: 'Group ID is required for group type accounts' })
3266
  }
3267
 
 
3272
  }
3273
 
3274
  // 处理���组的变更
3275
+ if (mappedUpdates.accountType !== undefined) {
3276
  // 如果之前是分组类型,需要从所有分组中移除
3277
  if (currentAccount.accountType === 'group') {
3278
  const oldGroups = await accountGroupService.getAccountGroups(accountId)
 
3281
  }
3282
  }
3283
  // 如果新类型是分组,处理多分组支持
3284
+ if (mappedUpdates.accountType === 'group') {
3285
+ if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'groupIds')) {
3286
  // 如果明确提供了 groupIds 参数(包括空数组)
3287
+ if (mappedUpdates.groupIds && mappedUpdates.groupIds.length > 0) {
3288
  // 设置新的多分组
3289
+ await accountGroupService.setAccountGroups(accountId, mappedUpdates.groupIds, 'claude')
3290
  } else {
3291
  // groupIds 为空数组,从所有分组中移除
3292
  await accountGroupService.removeAccountFromAllGroups(accountId)
3293
  }
3294
+ } else if (mappedUpdates.groupId) {
3295
  // 向后兼容:仅当没有 groupIds 但有 groupId 时使用单分组逻辑
3296
+ await accountGroupService.addAccountToGroup(accountId, mappedUpdates.groupId, 'claude')
3297
  }
3298
  }
3299
  }
3300
 
 
 
 
 
 
 
 
 
 
 
 
3301
  await ccrAccountService.updateAccount(accountId, mappedUpdates)
3302
 
3303
  logger.success(`📝 Admin updated CCR account: ${accountId}`)
 
3512
  // 为每个账户添加使用统计信息
3513
  const accountsWithStats = await Promise.all(
3514
  accounts.map(async (account) => {
 
3515
  try {
3516
  const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
3517
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
3518
 
3519
+ const formattedAccount = formatAccountExpiry(account)
3520
  return {
3521
  ...formattedAccount,
3522
  groupInfos,
 
3533
  )
3534
  try {
3535
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
3536
+ const formattedAccount = formatAccountExpiry(account)
3537
  return {
3538
  ...formattedAccount,
3539
  groupInfos,
 
3549
  groupError.message
3550
  )
3551
  return {
3552
+ ...account,
3553
  groupInfos: [],
3554
  usage: {
3555
  daily: { tokens: 0, requests: 0, allTokens: 0 },
 
3562
  })
3563
  )
3564
 
3565
+ return res.json({ success: true, data: accountsWithStats })
 
3566
  } catch (error) {
3567
  logger.error('❌ Failed to get Bedrock accounts:', error)
3568
  return res.status(500).json({ error: 'Failed to get Bedrock accounts', message: error.message })
 
3624
  }
3625
 
3626
  logger.success(`☁️ Admin created Bedrock account: ${name}`)
3627
+ const formattedAccount = formatAccountExpiry(formattedAccount)
3628
+ return res.json({ success: true, data: result.data })
3629
  } catch (error) {
3630
  logger.error('❌ Failed to create Bedrock account:', error)
3631
  return res
 
3640
  const { accountId } = req.params
3641
  const updates = req.body
3642
 
3643
+ // ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
3644
+ const mappedUpdates = mapExpiryField(updates, 'Bedrock', accountId)
3645
+
3646
  // 验证priority的有效性(1-100)
3647
+ if (
3648
+ mappedUpdates.priority !== undefined &&
3649
+ (mappedUpdates.priority < 1 || mappedUpdates.priority > 100)
3650
+ ) {
3651
  return res.status(400).json({ error: 'Priority must be between 1 and 100' })
3652
  }
3653
 
3654
  // 验证accountType的有效性
3655
+ if (mappedUpdates.accountType && !['shared', 'dedicated'].includes(mappedUpdates.accountType)) {
3656
  return res
3657
  .status(400)
3658
  .json({ error: 'Invalid account type. Must be "shared" or "dedicated"' })
 
3660
 
3661
  // 验证credentialType的有效性
3662
  if (
3663
+ mappedUpdates.credentialType &&
3664
+ !['default', 'access_key', 'bearer_token'].includes(mappedUpdates.credentialType)
3665
  ) {
3666
  return res.status(400).json({
3667
  error: 'Invalid credential type. Must be "default", "access_key", or "bearer_token"'
3668
  })
3669
  }
3670
 
 
 
 
 
 
 
 
 
 
 
 
3671
  const result = await bedrockAccountService.updateAccount(accountId, mappedUpdates)
3672
 
3673
  if (!result.success) {
 
3987
  // 为每个账户添加使用统计信息(与Claude账户相同的逻辑)
3988
  const accountsWithStats = await Promise.all(
3989
  accounts.map(async (account) => {
 
3990
  try {
3991
  const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
3992
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
3993
 
3994
+ const formattedAccount = formatAccountExpiry(account)
3995
  return {
3996
  ...formattedAccount,
3997
  groupInfos,
 
4009
  // 如果获取统计失败,返回空统计
4010
  try {
4011
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
4012
+ const formattedAccount = formatAccountExpiry(account)
4013
  return {
4014
  ...formattedAccount,
4015
  groupInfos,
 
4025
  groupError.message
4026
  )
4027
  return {
4028
+ ...account,
4029
  groupInfos: [],
4030
  usage: {
4031
  daily: { tokens: 0, requests: 0, allTokens: 0 },
 
4038
  })
4039
  )
4040
 
4041
+ return res.json({ success: true, data: accountsWithStats })
 
4042
  } catch (error) {
4043
  logger.error('❌ Failed to get Gemini accounts:', error)
4044
  return res.status(500).json({ error: 'Failed to get accounts', message: error.message })
 
4078
  }
4079
 
4080
  logger.success(`🏢 Admin created new Gemini account: ${accountData.name}`)
4081
+ const formattedAccount = formatAccountExpiry(formattedAccount)
4082
+ return res.json({ success: true, data: newAccount })
4083
  } catch (error) {
4084
  logger.error('❌ Failed to create Gemini account:', error)
4085
  return res.status(500).json({ error: 'Failed to create account', message: error.message })
 
4110
  return res.status(404).json({ error: 'Account not found' })
4111
  }
4112
 
4113
+ // ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
4114
+ const mappedUpdates = mapExpiryField(updates, 'Gemini', accountId)
4115
+
4116
  // 处理分组的变更
4117
+ if (mappedUpdates.accountType !== undefined) {
4118
  // 如果之前是分组类型,需要从所有分组中移除
4119
  if (currentAccount.accountType === 'group') {
4120
  const oldGroups = await accountGroupService.getAccountGroups(accountId)
 
4123
  }
4124
  }
4125
  // 如果新类型是分组,处理多分组支持
4126
+ if (mappedUpdates.accountType === 'group') {
4127
+ if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'groupIds')) {
4128
  // 如果明确提供了 groupIds 参数(包括空数组)
4129
+ if (mappedUpdates.groupIds && mappedUpdates.groupIds.length > 0) {
4130
  // 设置新的多分组
4131
+ await accountGroupService.setAccountGroups(accountId, mappedUpdates.groupIds, 'gemini')
4132
  } else {
4133
  // groupIds 为空数组,从所有分组中移除
4134
  await accountGroupService.removeAccountFromAllGroups(accountId)
4135
  }
4136
+ } else if (mappedUpdates.groupId) {
4137
  // 向后兼容:仅当没有 groupIds 但有 groupId 时使用单分组逻辑
4138
+ await accountGroupService.addAccountToGroup(accountId, mappedUpdates.groupId, 'gemini')
4139
  }
4140
  }
4141
  }
4142
 
 
 
 
 
 
 
 
 
 
 
 
4143
  const updatedAccount = await geminiAccountService.updateAccount(accountId, mappedUpdates)
4144
 
4145
  logger.success(`📝 Admin updated Gemini account: ${accountId}`)
4146
+ return res.json({ success: true, data: updatedAccount })
 
4147
  } catch (error) {
4148
  logger.error('❌ Failed to update Gemini account:', error)
4149
  return res.status(500).json({ error: 'Failed to update account', message: error.message })
 
7291
  try {
7292
  const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
7293
  const groupInfos = await fetchAccountGroups(account.id)
7294
+ const formattedAccount = formatAccountExpiry(account)
7295
  return {
7296
  ...formattedAccount,
7297
  groupInfos,
 
7304
  } catch (error) {
7305
  logger.debug(`Failed to get usage stats for OpenAI account ${account.id}:`, error)
7306
  const groupInfos = await fetchAccountGroups(account.id)
7307
+ const formattedAccount = formatAccountExpiry(account)
7308
  return {
7309
  ...formattedAccount,
7310
  groupInfos,
 
7320
 
7321
  logger.info(`获取 OpenAI 账户列表: ${accountsWithStats.length} 个账户`)
7322
 
 
 
7323
  return res.json({
7324
  success: true,
7325
+ data: accountsWithStats
7326
  })
7327
  } catch (error) {
7328
  logger.error('获取 OpenAI 账户列表失败:', error)
 
7348
  rateLimitDuration,
7349
  priority,
7350
  needsImmediateRefresh, // 是否需要立即刷新
7351
+ requireRefreshSuccess // 是否必须刷新成功才能创建
 
7352
  } = req.body
7353
 
7354
  if (!name) {
 
7370
  accountInfo: accountInfo || {},
7371
  proxy: proxy || null,
7372
  isActive: true,
7373
+ schedulable: true
 
7374
  }
7375
 
7376
  // 如果需要立即刷新且必须成功(OpenAI 手动模式)
 
7406
 
7407
  logger.success(`✅ ��建并验证 OpenAI 账户成功: ${name} (ID: ${tempAccount.id})`)
7408
 
 
 
7409
  return res.json({
7410
  success: true,
7411
+ data: refreshedAccount,
7412
  message: '账户创建成功,并已获取完整 token 信息'
7413
  })
7414
  } catch (refreshError) {
 
7470
 
7471
  logger.success(`✅ 创建 OpenAI 账户成功: ${name} (ID: ${createdAccount.id})`)
7472
 
 
 
7473
  return res.json({
7474
  success: true,
7475
+ data: createdAccount
7476
  })
7477
  } catch (error) {
7478
  logger.error('创建 OpenAI 账户失败:', error)
 
7489
  try {
7490
  const { id } = req.params
7491
  const updates = req.body
7492
+
7493
+ // ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
7494
+ const mappedUpdates = mapExpiryField(updates, 'OpenAI', id)
7495
+
7496
+ const { needsImmediateRefresh, requireRefreshSuccess } = mappedUpdates
7497
 
7498
  // 验证accountType的有效性
7499
+ if (
7500
+ mappedUpdates.accountType &&
7501
+ !['shared', 'dedicated', 'group'].includes(mappedUpdates.accountType)
7502
+ ) {
7503
  return res
7504
  .status(400)
7505
  .json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
7506
  }
7507
 
7508
  // 如果更新为分组类型,验证groupId
7509
+ if (mappedUpdates.accountType === 'group' && !mappedUpdates.groupId) {
7510
  return res.status(400).json({ error: 'Group ID is required for group type accounts' })
7511
  }
7512
 
 
7517
  }
7518
 
7519
  // 如果更新了 Refresh Token,需要验证其有效性
7520
+ if (mappedUpdates.openaiOauth?.refreshToken && needsImmediateRefresh && requireRefreshSuccess) {
7521
  // 先更新 token 信息
7522
  const tempUpdateData = {}
7523
+ if (mappedUpdates.openaiOauth.refreshToken) {
7524
+ tempUpdateData.refreshToken = mappedUpdates.openaiOauth.refreshToken
7525
  }
7526
+ if (mappedUpdates.openaiOauth.accessToken) {
7527
+ tempUpdateData.accessToken = mappedUpdates.openaiOauth.accessToken
7528
  }
7529
  // 更新代理配置(如果有)
7530
+ if (mappedUpdates.proxy !== undefined) {
7531
+ tempUpdateData.proxy = mappedUpdates.proxy
7532
  }
7533
 
7534
  // 临时更新账户以测试新的 token
 
7604
  }
7605
 
7606
  // 处理分组的变更
7607
+ if (mappedUpdates.accountType !== undefined) {
7608
  // 如果之前是分组类型,需要从原分组中移除
7609
  if (currentAccount.accountType === 'group') {
7610
  const oldGroup = await accountGroupService.getAccountGroup(id)
 
7613
  }
7614
  }
7615
  // 如果新类型是分组,添加到新分组
7616
+ if (mappedUpdates.accountType === 'group' && mappedUpdates.groupId) {
7617
+ await accountGroupService.addAccountToGroup(id, mappedUpdates.groupId, 'openai')
7618
  }
7619
  }
7620
 
7621
  // 准备更新数据
7622
+ const updateData = { ...mappedUpdates }
7623
 
7624
  // 处理敏感数据加密
7625
+ if (mappedUpdates.openaiOauth) {
7626
+ updateData.openaiOauth = mappedUpdates.openaiOauth
7627
  // 编辑时不允许直接输入 ID Token,只能通过刷新获取
7628
+ if (mappedUpdates.openaiOauth.accessToken) {
7629
+ updateData.accessToken = mappedUpdates.openaiOauth.accessToken
7630
  }
7631
+ if (mappedUpdates.openaiOauth.refreshToken) {
7632
+ updateData.refreshToken = mappedUpdates.openaiOauth.refreshToken
7633
  }
7634
+ if (mappedUpdates.openaiOauth.expires_in) {
7635
  updateData.expiresAt = new Date(
7636
+ Date.now() + mappedUpdates.openaiOauth.expires_in * 1000
7637
  ).toISOString()
7638
  }
7639
  }
7640
 
7641
  // 更新账户信息
7642
+ if (mappedUpdates.accountInfo) {
7643
+ updateData.accountId = mappedUpdates.accountInfo.accountId || currentAccount.accountId
7644
+ updateData.chatgptUserId =
7645
+ mappedUpdates.accountInfo.chatgptUserId || currentAccount.chatgptUserId
7646
  updateData.organizationId =
7647
+ mappedUpdates.accountInfo.organizationId || currentAccount.organizationId
7648
  updateData.organizationRole =
7649
+ mappedUpdates.accountInfo.organizationRole || currentAccount.organizationRole
7650
  updateData.organizationTitle =
7651
+ mappedUpdates.accountInfo.organizationTitle || currentAccount.organizationTitle
7652
+ updateData.planType = mappedUpdates.accountInfo.planType || currentAccount.planType
7653
+ updateData.email = mappedUpdates.accountInfo.email || currentAccount.email
7654
  updateData.emailVerified =
7655
+ mappedUpdates.accountInfo.emailVerified !== undefined
7656
+ ? mappedUpdates.accountInfo.emailVerified
7657
  : currentAccount.emailVerified
7658
  }
7659
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7660
  const updatedAccount = await openaiAccountService.updateAccount(id, updateData)
7661
 
7662
  // 如果需要刷新但不强制成功(非关键更新)
 
7671
  }
7672
 
7673
  logger.success(`📝 Admin updated OpenAI account: ${id}`)
7674
+ return res.json({ success: true, data: updatedAccount })
 
7675
  } catch (error) {
7676
  logger.error('❌ Failed to update OpenAI account:', error)
7677
  return res.status(500).json({ error: 'Failed to update account', message: error.message })
 
7752
  `✅ ${account.enabled ? '启用' : '禁用'} OpenAI 账户: ${account.name} (ID: ${id})`
7753
  )
7754
 
 
 
7755
  return res.json({
7756
  success: true,
7757
+ data: account
7758
  })
7759
  } catch (error) {
7760
  logger.error('切换 OpenAI 账户状态失败:', error)
 
7860
  // 为每个账户添加使用统计信息和分组信息
7861
  const accountsWithStats = await Promise.all(
7862
  accounts.map(async (account) => {
 
7863
  try {
7864
  const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
7865
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
7866
+ const formattedAccount = formatAccountExpiry(account)
7867
  return {
7868
  ...formattedAccount,
7869
  groupInfos,
 
7877
  logger.debug(`Failed to get usage stats for Azure OpenAI account ${account.id}:`, error)
7878
  try {
7879
  const groupInfos = await accountGroupService.getAccountGroups(account.id)
7880
+ const formattedAccount = formatAccountExpiry(account)
7881
  return {
7882
  ...formattedAccount,
7883
  groupInfos,
 
7890
  } catch (groupError) {
7891
  logger.debug(`Failed to get group info for account ${account.id}:`, groupError)
7892
  return {
7893
+ ...account,
7894
  groupInfos: [],
7895
  usage: {
7896
  daily: { requests: 0, tokens: 0, allTokens: 0 },
 
7903
  })
7904
  )
7905
 
 
 
7906
  res.json({
7907
  success: true,
7908
+ data: accountsWithStats
7909
  })
7910
  } catch (error) {
7911
  logger.error('Failed to fetch Azure OpenAI accounts:', error)
 
8024
  }
8025
  }
8026
 
 
 
8027
  res.json({
8028
  success: true,
8029
+ data: account,
8030
  message: 'Azure OpenAI account created successfully'
8031
  })
8032
  } catch (error) {
 
8045
  const { id } = req.params
8046
  const updates = req.body
8047
 
8048
+ // ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
8049
+ const mappedUpdates = mapExpiryField(updates, 'Azure OpenAI', id)
 
 
 
 
 
 
 
 
8050
 
8051
  const account = await azureOpenaiAccountService.updateAccount(id, mappedUpdates)
 
8052
 
8053
  res.json({
8054
  success: true,
8055
+ data: account,
8056
  message: 'Azure OpenAI account updated successfully'
8057
  })
8058
  } catch (error) {
 
8305
  // 处理额度信息、使用统计和绑定的 API Key 数量
8306
  const accountsWithStats = await Promise.all(
8307
  accounts.map(async (account) => {
 
8308
  try {
8309
  // 检查是否需要重置额度
8310
  const today = redis.getDateStringInTimezone()
 
8358
  logger.info(`OpenAI-Responses account ${account.id} has ${boundCount} bound API keys`)
8359
  }
8360
 
8361
+ const formattedAccount = formatAccountExpiry(account)
8362
  return {
8363
  ...formattedAccount,
8364
  boundApiKeysCount: boundCount,
 
8370
  }
8371
  } catch (error) {
8372
  logger.error(`Failed to process OpenAI-Responses account ${account.id}:`, error)
8373
+ const formattedAccount = formatAccountExpiry(account)
8374
  return {
8375
  ...formattedAccount,
8376
  boundApiKeysCount: 0,
 
8384
  })
8385
  )
8386
 
8387
+ res.json({ success: true, data: accountsWithStats })
 
 
8388
  } catch (error) {
8389
  logger.error('Failed to get OpenAI-Responses accounts:', error)
8390
  res.status(500).json({ success: false, message: error.message })
 
8395
  router.post('/openai-responses-accounts', authenticateAdmin, async (req, res) => {
8396
  try {
8397
  const account = await openaiResponsesAccountService.createAccount(req.body)
8398
+ const formattedAccount = formatAccountExpiry(account)
8399
+ res.json({ success: true, data: formattedAccount })
8400
  } catch (error) {
8401
  logger.error('Failed to create OpenAI-Responses account:', error)
8402
  res.status(500).json({
 
8412
  const { id } = req.params
8413
  const updates = req.body
8414
 
8415
+ // ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
8416
+ const mappedUpdates = mapExpiryField(updates, 'OpenAI-Responses', id)
8417
+
8418
  // 验证priority的有效性(1-100)
8419
+ if (mappedUpdates.priority !== undefined) {
8420
+ const priority = parseInt(mappedUpdates.priority)
8421
  if (isNaN(priority) || priority < 1 || priority > 100) {
8422
  return res.status(400).json({
8423
  success: false,
8424
  message: 'Priority must be a number between 1 and 100'
8425
  })
8426
  }
8427
+ mappedUpdates.priority = priority.toString()
 
 
 
 
 
 
 
 
 
 
 
8428
  }
8429
 
8430
  const result = await openaiResponsesAccountService.updateAccount(id, mappedUpdates)
 
8433
  return res.status(400).json(result)
8434
  }
8435
 
8436
+ res.json({ success: true, ...result })
 
 
 
 
 
 
8437
  } catch (error) {
8438
  logger.error('Failed to update OpenAI-Responses account:', error)
8439
  res.status(500).json({
 
8763
  // 添加使用统计
8764
  const accountsWithStats = await Promise.all(
8765
  accounts.map(async (account) => {
 
8766
  try {
8767
  const usageStats = await redis.getAccountUsageStats(account.id, 'droid')
8768
  let groupInfos = []
 
8791
  return count
8792
  }, 0)
8793
 
8794
+ const formattedAccount = formatAccountExpiry(account)
8795
  return {
8796
  ...formattedAccount,
8797
  schedulable: account.schedulable === 'true',
 
8805
  }
8806
  } catch (error) {
8807
  logger.warn(`Failed to get stats for Droid account ${account.id}:`, error.message)
8808
+ const formattedAccount = formatAccountExpiry(account)
8809
  return {
8810
  ...formattedAccount,
8811
  boundApiKeysCount: 0,
 
8820
  })
8821
  )
8822
 
8823
+ return res.json({ success: true, data: accountsWithStats })
 
 
8824
  } catch (error) {
8825
  logger.error('Failed to get Droid accounts:', error)
8826
  return res.status(500).json({ error: 'Failed to get Droid accounts', message: error.message })
 
8877
  }
8878
 
8879
  logger.success(`Created Droid account: ${account.name} (${account.id})`)
8880
+ const formattedAccount = formatAccountExpiry(account)
8881
+ return res.json({ success: true, data: formattedAccount })
8882
  } catch (error) {
8883
  logger.error('Failed to create Droid account:', error)
8884
  return res.status(500).json({ error: 'Failed to create Droid account', message: error.message })
 
8890
  try {
8891
  const { id } = req.params
8892
  const updates = { ...req.body }
8893
+
8894
+ // ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
8895
+ const mappedUpdates = mapExpiryField(updates, 'Droid', id)
8896
+
8897
+ const { accountType: rawAccountType, groupId, groupIds } = mappedUpdates
8898
 
8899
  if (rawAccountType && !['shared', 'dedicated', 'group'].includes(rawAccountType)) {
8900
  return res.status(400).json({ error: '账户类型必须是 shared、dedicated 或 group' })
 
8916
  const normalizedGroupIds = Array.isArray(groupIds)
8917
  ? groupIds.filter((gid) => typeof gid === 'string' && gid.trim())
8918
  : []
8919
+ const hasGroupIdsField = Object.prototype.hasOwnProperty.call(mappedUpdates, 'groupIds')
8920
+ const hasGroupIdField = Object.prototype.hasOwnProperty.call(mappedUpdates, 'groupId')
8921
  const targetAccountType = rawAccountType || currentAccount.accountType || 'shared'
8922
 
8923
+ delete mappedUpdates.groupId
8924
+ delete mappedUpdates.groupIds
8925
 
8926
  if (rawAccountType) {
8927
+ mappedUpdates.accountType = targetAccountType
 
 
 
 
 
 
 
 
 
 
 
8928
  }
8929
 
8930
  const account = await droidAccountService.updateAccount(id, mappedUpdates)
 
8959
  }
8960
  }
8961
 
8962
+ return res.json({ success: true, data: account })
 
8963
  } catch (error) {
8964
  logger.error(`Failed to update Droid account ${req.params.id}:`, error)
8965
  return res.status(500).json({ error: 'Failed to update Droid account', message: error.message })
src/routes/api.js CHANGED
@@ -11,7 +11,6 @@ const logger = require('../utils/logger')
11
  const { getEffectiveModel, parseVendorPrefixedModel } = require('../utils/modelHelper')
12
  const sessionHelper = require('../utils/sessionHelper')
13
  const { updateRateLimitCounters } = require('../utils/rateLimitHelper')
14
-
15
  const router = express.Router()
16
 
17
  function queueRateLimitUpdate(rateLimitInfo, usageSummary, model, context = '') {
@@ -722,40 +721,23 @@ router.post('/v1/messages', authenticateApiKey, handleMessagesRequest)
722
  // 🚀 Claude API messages 端点 - /claude/v1/messages (别名)
723
  router.post('/claude/v1/messages', authenticateApiKey, handleMessagesRequest)
724
 
725
- // 📋 模型列表端点 - Claude Code 客户端需要
726
  router.get('/v1/models', authenticateApiKey, async (req, res) => {
727
  try {
728
- // 返回支持的模型列表
729
- const models = [
730
- {
731
- id: 'claude-3-5-sonnet-20241022',
732
- object: 'model',
733
- created: 1669599635,
734
- owned_by: 'anthropic'
735
- },
736
- {
737
- id: 'claude-3-5-haiku-20241022',
738
- object: 'model',
739
- created: 1669599635,
740
- owned_by: 'anthropic'
741
- },
742
- {
743
- id: 'claude-3-opus-20240229',
744
- object: 'model',
745
- created: 1669599635,
746
- owned_by: 'anthropic'
747
- },
748
- {
749
- id: 'claude-sonnet-4-20250514',
750
- object: 'model',
751
- created: 1669599635,
752
- owned_by: 'anthropic'
753
- }
754
- ]
755
 
756
  res.json({
757
  object: 'list',
758
- data: models
759
  })
760
  } catch (error) {
761
  logger.error('❌ Models list error:', error)
 
11
  const { getEffectiveModel, parseVendorPrefixedModel } = require('../utils/modelHelper')
12
  const sessionHelper = require('../utils/sessionHelper')
13
  const { updateRateLimitCounters } = require('../utils/rateLimitHelper')
 
14
  const router = express.Router()
15
 
16
  function queueRateLimitUpdate(rateLimitInfo, usageSummary, model, context = '') {
 
721
  // 🚀 Claude API messages 端点 - /claude/v1/messages (别名)
722
  router.post('/claude/v1/messages', authenticateApiKey, handleMessagesRequest)
723
 
724
+ // 📋 模型列表端点 - 支持 Claude, OpenAI, Gemini
725
  router.get('/v1/models', authenticateApiKey, async (req, res) => {
726
  try {
727
+ const modelService = require('../services/modelService')
728
+
729
+ // 从 modelService 获取所有支持的模型
730
+ const models = modelService.getAllModels()
731
+
732
+ // 可选:根据 API Key 的模型限制过滤
733
+ let filteredModels = models
734
+ if (req.apiKey.enableModelRestriction && req.apiKey.restrictedModels?.length > 0) {
735
+ filteredModels = models.filter((model) => req.apiKey.restrictedModels.includes(model.id))
736
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
737
 
738
  res.json({
739
  object: 'list',
740
+ data: filteredModels
741
  })
742
  } catch (error) {
743
  logger.error('❌ Models list error:', error)
src/routes/droidRoutes.js CHANGED
@@ -60,49 +60,6 @@ router.post('/claude/v1/messages', authenticateApiKey, async (req, res) => {
60
  }
61
  })
62
 
63
- router.post('/claude/v1/messages/count_tokens', authenticateApiKey, async (req, res) => {
64
- try {
65
- const requestBody = { ...req.body }
66
- if ('stream' in requestBody) {
67
- delete requestBody.stream
68
- }
69
- const sessionHash = sessionHelper.generateSessionHash(requestBody)
70
-
71
- if (!hasDroidPermission(req.apiKey)) {
72
- logger.security(
73
- `🚫 API Key ${req.apiKey?.id || 'unknown'} 缺少 Droid 权限,拒绝访问 ${req.originalUrl}`
74
- )
75
- return res.status(403).json({
76
- error: 'permission_denied',
77
- message: '此 API Key 未启用 Droid 权限'
78
- })
79
- }
80
-
81
- const result = await droidRelayService.relayRequest(
82
- requestBody,
83
- req.apiKey,
84
- req,
85
- res,
86
- req.headers,
87
- {
88
- endpointType: 'anthropic',
89
- sessionHash,
90
- customPath: '/a/v1/messages/count_tokens',
91
- skipUsageRecord: true,
92
- disableStreaming: true
93
- }
94
- )
95
-
96
- res.status(result.statusCode).set(result.headers).send(result.body)
97
- } catch (error) {
98
- logger.error('Droid Claude count_tokens relay error:', error)
99
- res.status(500).json({
100
- error: 'internal_server_error',
101
- message: error.message
102
- })
103
- }
104
- })
105
-
106
  // OpenAI 端点 - /v1/responses
107
  router.post(['/openai/v1/responses', '/openai/responses'], authenticateApiKey, async (req, res) => {
108
  try {
 
60
  }
61
  })
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  // OpenAI 端点 - /v1/responses
64
  router.post(['/openai/v1/responses', '/openai/responses'], authenticateApiKey, async (req, res) => {
65
  try {
src/routes/openaiClaudeRoutes.js CHANGED
@@ -490,3 +490,4 @@ router.post('/v1/completions', authenticateApiKey, async (req, res) => {
490
  })
491
 
492
  module.exports = router
 
 
490
  })
491
 
492
  module.exports = router
493
+ module.exports.handleChatCompletion = handleChatCompletion
src/routes/openaiRoutes.js CHANGED
@@ -919,3 +919,4 @@ router.get('/key-info', authenticateApiKey, async (req, res) => {
919
  })
920
 
921
  module.exports = router
 
 
919
  })
920
 
921
  module.exports = router
922
+ module.exports.handleResponses = handleResponses
src/routes/unified.js ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express')
2
+ const { authenticateApiKey } = require('../middleware/auth')
3
+ const logger = require('../utils/logger')
4
+ const { handleChatCompletion } = require('./openaiClaudeRoutes')
5
+ const {
6
+ handleGenerateContent: geminiHandleGenerateContent,
7
+ handleStreamGenerateContent: geminiHandleStreamGenerateContent
8
+ } = require('./geminiRoutes')
9
+ const openaiRoutes = require('./openaiRoutes')
10
+
11
+ const router = express.Router()
12
+
13
+ // 🔍 根据模型名称检测后端类型
14
+ function detectBackendFromModel(modelName) {
15
+ if (!modelName) {
16
+ return 'claude' // 默认 Claude
17
+ }
18
+
19
+ // 首先尝试使用 modelService 查找模型的 provider
20
+ try {
21
+ const modelService = require('../services/modelService')
22
+ const provider = modelService.getModelProvider(modelName)
23
+
24
+ if (provider === 'anthropic') {
25
+ return 'claude'
26
+ }
27
+ if (provider === 'openai') {
28
+ return 'openai'
29
+ }
30
+ if (provider === 'google') {
31
+ return 'gemini'
32
+ }
33
+ } catch (error) {
34
+ logger.warn(`⚠️ Failed to detect backend from modelService: ${error.message}`)
35
+ }
36
+
37
+ // 降级到前缀匹配作为后备方案
38
+ const model = modelName.toLowerCase()
39
+
40
+ // Claude 模型
41
+ if (model.startsWith('claude-')) {
42
+ return 'claude'
43
+ }
44
+
45
+ // OpenAI 模型
46
+ if (
47
+ model.startsWith('gpt-') ||
48
+ model.startsWith('o1-') ||
49
+ model.startsWith('o3-') ||
50
+ model === 'chatgpt-4o-latest'
51
+ ) {
52
+ return 'openai'
53
+ }
54
+
55
+ // Gemini 模型
56
+ if (model.startsWith('gemini-')) {
57
+ return 'gemini'
58
+ }
59
+
60
+ // 默认使用 Claude
61
+ return 'claude'
62
+ }
63
+
64
+ // 🚀 智能后端路由处理器
65
+ async function routeToBackend(req, res, requestedModel) {
66
+ const backend = detectBackendFromModel(requestedModel)
67
+
68
+ logger.info(`🔀 Routing request - Model: ${requestedModel}, Backend: ${backend}`)
69
+
70
+ // 检查权限
71
+ const permissions = req.apiKey.permissions || 'all'
72
+
73
+ if (backend === 'claude') {
74
+ // Claude 后端:通过 OpenAI 兼容层
75
+ if (permissions !== 'all' && permissions !== 'claude') {
76
+ return res.status(403).json({
77
+ error: {
78
+ message: 'This API key does not have permission to access Claude',
79
+ type: 'permission_denied',
80
+ code: 'permission_denied'
81
+ }
82
+ })
83
+ }
84
+ await handleChatCompletion(req, res, req.apiKey)
85
+ } else if (backend === 'openai') {
86
+ // OpenAI 后端
87
+ if (permissions !== 'all' && permissions !== 'openai') {
88
+ return res.status(403).json({
89
+ error: {
90
+ message: 'This API key does not have permission to access OpenAI',
91
+ type: 'permission_denied',
92
+ code: 'permission_denied'
93
+ }
94
+ })
95
+ }
96
+ return await openaiRoutes.handleResponses(req, res)
97
+ } else if (backend === 'gemini') {
98
+ // Gemini 后端
99
+ if (permissions !== 'all' && permissions !== 'gemini') {
100
+ return res.status(403).json({
101
+ error: {
102
+ message: 'This API key does not have permission to access Gemini',
103
+ type: 'permission_denied',
104
+ code: 'permission_denied'
105
+ }
106
+ })
107
+ }
108
+
109
+ // 转换为 Gemini 格式
110
+ const geminiRequest = {
111
+ model: requestedModel,
112
+ messages: req.body.messages,
113
+ temperature: req.body.temperature || 0.7,
114
+ max_tokens: req.body.max_tokens || 4096,
115
+ stream: req.body.stream || false
116
+ }
117
+
118
+ req.body = geminiRequest
119
+
120
+ if (geminiRequest.stream) {
121
+ return await geminiHandleStreamGenerateContent(req, res)
122
+ } else {
123
+ return await geminiHandleGenerateContent(req, res)
124
+ }
125
+ } else {
126
+ return res.status(500).json({
127
+ error: {
128
+ message: `Unsupported backend: ${backend}`,
129
+ type: 'server_error',
130
+ code: 'unsupported_backend'
131
+ }
132
+ })
133
+ }
134
+ }
135
+
136
+ // 🔄 OpenAI 兼容的 chat/completions 端点(智能后端路由)
137
+ router.post('/v1/chat/completions', authenticateApiKey, async (req, res) => {
138
+ try {
139
+ // 验证必需参数
140
+ if (!req.body.messages || !Array.isArray(req.body.messages) || req.body.messages.length === 0) {
141
+ return res.status(400).json({
142
+ error: {
143
+ message: 'Messages array is required and cannot be empty',
144
+ type: 'invalid_request_error',
145
+ code: 'invalid_request'
146
+ }
147
+ })
148
+ }
149
+
150
+ const requestedModel = req.body.model || 'claude-3-5-sonnet-20241022'
151
+ req.body.model = requestedModel // 确保模型已设置
152
+
153
+ // 使用统一的后端路由处理器
154
+ await routeToBackend(req, res, requestedModel)
155
+ } catch (error) {
156
+ logger.error('❌ OpenAI chat/completions error:', error)
157
+ if (!res.headersSent) {
158
+ res.status(500).json({
159
+ error: {
160
+ message: 'Internal server error',
161
+ type: 'server_error',
162
+ code: 'internal_error'
163
+ }
164
+ })
165
+ }
166
+ }
167
+ })
168
+
169
+ // 🔄 OpenAI 兼容的 completions 端点(传统格式,智能后端路由)
170
+ router.post('/v1/completions', authenticateApiKey, async (req, res) => {
171
+ try {
172
+ // 验证必���参数
173
+ if (!req.body.prompt) {
174
+ return res.status(400).json({
175
+ error: {
176
+ message: 'Prompt is required',
177
+ type: 'invalid_request_error',
178
+ code: 'invalid_request'
179
+ }
180
+ })
181
+ }
182
+
183
+ // 将传统 completions 格式转换为 chat 格式
184
+ const originalBody = req.body
185
+ const requestedModel = originalBody.model || 'claude-3-5-sonnet-20241022'
186
+
187
+ req.body = {
188
+ model: requestedModel,
189
+ messages: [
190
+ {
191
+ role: 'user',
192
+ content: originalBody.prompt
193
+ }
194
+ ],
195
+ max_tokens: originalBody.max_tokens,
196
+ temperature: originalBody.temperature,
197
+ top_p: originalBody.top_p,
198
+ stream: originalBody.stream,
199
+ stop: originalBody.stop,
200
+ n: originalBody.n || 1,
201
+ presence_penalty: originalBody.presence_penalty,
202
+ frequency_penalty: originalBody.frequency_penalty,
203
+ logit_bias: originalBody.logit_bias,
204
+ user: originalBody.user
205
+ }
206
+
207
+ // 使用统一的后端路由处理器
208
+ await routeToBackend(req, res, requestedModel)
209
+ } catch (error) {
210
+ logger.error('❌ OpenAI completions error:', error)
211
+ if (!res.headersSent) {
212
+ res.status(500).json({
213
+ error: {
214
+ message: 'Failed to process completion request',
215
+ type: 'server_error',
216
+ code: 'internal_error'
217
+ }
218
+ })
219
+ }
220
+ }
221
+ })
222
+
223
+ module.exports = router
224
+ module.exports.detectBackendFromModel = detectBackendFromModel
225
+ module.exports.routeToBackend = routeToBackend
src/services/azureOpenaiAccountService.js CHANGED
@@ -65,19 +65,6 @@ const AZURE_OPENAI_ACCOUNT_KEY_PREFIX = 'azure_openai:account:'
65
  const SHARED_AZURE_OPENAI_ACCOUNTS_KEY = 'shared_azure_openai_accounts'
66
  const ACCOUNT_SESSION_MAPPING_PREFIX = 'azure_openai_session_account_mapping:'
67
 
68
- function normalizeSubscriptionExpiresAt(value) {
69
- if (value === undefined || value === null || value === '') {
70
- return ''
71
- }
72
-
73
- const date = value instanceof Date ? value : new Date(value)
74
- if (Number.isNaN(date.getTime())) {
75
- return ''
76
- }
77
-
78
- return date.toISOString()
79
- }
80
-
81
  // 加密函数
82
  function encrypt(text) {
83
  if (!text) {
@@ -142,11 +129,15 @@ async function createAccount(accountData) {
142
  supportedModels: JSON.stringify(
143
  accountData.supportedModels || ['gpt-4', 'gpt-4-turbo', 'gpt-35-turbo', 'gpt-35-turbo-16k']
144
  ),
 
 
 
 
 
145
  // 状态字段
146
  isActive: accountData.isActive !== false ? 'true' : 'false',
147
  status: 'active',
148
  schedulable: accountData.schedulable !== false ? 'true' : 'false',
149
- subscriptionExpiresAt: normalizeSubscriptionExpiresAt(accountData.subscriptionExpiresAt || ''),
150
  createdAt: now,
151
  updatedAt: now
152
  }
@@ -166,10 +157,7 @@ async function createAccount(accountData) {
166
  }
167
 
168
  logger.info(`Created Azure OpenAI account: ${accountId}`)
169
- return {
170
- ...account,
171
- subscriptionExpiresAt: account.subscriptionExpiresAt || null
172
- }
173
  }
174
 
175
  // 获取账户
@@ -204,11 +192,6 @@ async function getAccount(accountId) {
204
  }
205
  }
206
 
207
- accountData.subscriptionExpiresAt =
208
- accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== ''
209
- ? accountData.subscriptionExpiresAt
210
- : null
211
-
212
  return accountData
213
  }
214
 
@@ -240,11 +223,10 @@ async function updateAccount(accountId, updates) {
240
  : JSON.stringify(updates.supportedModels)
241
  }
242
 
243
- if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
244
- updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.subscriptionExpiresAt)
245
- } else if (Object.prototype.hasOwnProperty.call(updates, 'expiresAt')) {
246
- updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.expiresAt)
247
- delete updates.expiresAt
248
  }
249
 
250
  // 更新账户类型时处理共享账户集合
@@ -273,10 +255,6 @@ async function updateAccount(accountId, updates) {
273
  }
274
  }
275
 
276
- if (!updatedAccount.subscriptionExpiresAt) {
277
- updatedAccount.subscriptionExpiresAt = null
278
- }
279
-
280
  return updatedAccount
281
  }
282
 
@@ -337,7 +315,10 @@ async function getAllAccounts() {
337
  ...accountData,
338
  isActive: accountData.isActive === 'true',
339
  schedulable: accountData.schedulable !== 'false',
340
- subscriptionExpiresAt: accountData.subscriptionExpiresAt || null
 
 
 
341
  })
342
  }
343
  }
@@ -365,6 +346,19 @@ async function getSharedAccounts() {
365
  return accounts
366
  }
367
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  // 选择可用账户
369
  async function selectAvailableAccount(sessionId = null) {
370
  // 如果有会话ID,尝试获取之前分配的账户
@@ -386,9 +380,17 @@ async function selectAvailableAccount(sessionId = null) {
386
  const sharedAccounts = await getSharedAccounts()
387
 
388
  // 过滤出可用的账户
389
- const availableAccounts = sharedAccounts.filter(
390
- (acc) => acc.isActive === 'true' && acc.schedulable === 'true'
391
- )
 
 
 
 
 
 
 
 
392
 
393
  if (availableAccounts.length === 0) {
394
  throw new Error('No available Azure OpenAI accounts')
 
65
  const SHARED_AZURE_OPENAI_ACCOUNTS_KEY = 'shared_azure_openai_accounts'
66
  const ACCOUNT_SESSION_MAPPING_PREFIX = 'azure_openai_session_account_mapping:'
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  // 加密函数
69
  function encrypt(text) {
70
  if (!text) {
 
129
  supportedModels: JSON.stringify(
130
  accountData.supportedModels || ['gpt-4', 'gpt-4-turbo', 'gpt-35-turbo', 'gpt-35-turbo-16k']
131
  ),
132
+
133
+ // ✅ 新增:账户订阅到期时间(业务字段,手动管理)
134
+ // 注意:Azure OpenAI 使用 API Key 认证,没有 OAuth token,因此没有 expiresAt
135
+ subscriptionExpiresAt: accountData.subscriptionExpiresAt || null,
136
+
137
  // 状态字段
138
  isActive: accountData.isActive !== false ? 'true' : 'false',
139
  status: 'active',
140
  schedulable: accountData.schedulable !== false ? 'true' : 'false',
 
141
  createdAt: now,
142
  updatedAt: now
143
  }
 
157
  }
158
 
159
  logger.info(`Created Azure OpenAI account: ${accountId}`)
160
+ return account
 
 
 
161
  }
162
 
163
  // 获取账户
 
192
  }
193
  }
194
 
 
 
 
 
 
195
  return accountData
196
  }
197
 
 
223
  : JSON.stringify(updates.supportedModels)
224
  }
225
 
226
+ // 直接保存 subscriptionExpiresAt(如果提供)
227
+ // Azure OpenAI 使用 API Key,没有 token 刷新逻辑,不会覆盖此字段
228
+ if (updates.subscriptionExpiresAt !== undefined) {
229
+ // 直接保存,不做任何调整
 
230
  }
231
 
232
  // 更新账户类型时处理共享账户集合
 
255
  }
256
  }
257
 
 
 
 
 
258
  return updatedAccount
259
  }
260
 
 
315
  ...accountData,
316
  isActive: accountData.isActive === 'true',
317
  schedulable: accountData.schedulable !== 'false',
318
+
319
+ // ✅ 前端显示订阅过期时间(业务字段)
320
+ expiresAt: accountData.subscriptionExpiresAt || null,
321
+ platform: 'azure-openai'
322
  })
323
  }
324
  }
 
346
  return accounts
347
  }
348
 
349
+ /**
350
+ * 检查账户订阅是否过期
351
+ * @param {Object} account - 账户对象
352
+ * @returns {boolean} - true: 已过期, false: 未过期
353
+ */
354
+ function isSubscriptionExpired(account) {
355
+ if (!account.subscriptionExpiresAt) {
356
+ return false // 未设置视为永不过期
357
+ }
358
+ const expiryDate = new Date(account.subscriptionExpiresAt)
359
+ return expiryDate <= new Date()
360
+ }
361
+
362
  // 选择可用账户
363
  async function selectAvailableAccount(sessionId = null) {
364
  // 如果有会话ID,尝试获取之前分配的账户
 
380
  const sharedAccounts = await getSharedAccounts()
381
 
382
  // 过滤出可用的账户
383
+ const availableAccounts = sharedAccounts.filter((acc) => {
384
+ // 检查账户订阅是否过期
385
+ if (isSubscriptionExpired(acc)) {
386
+ logger.debug(
387
+ `⏰ Skipping expired Azure OpenAI account: ${acc.name}, expired at ${acc.subscriptionExpiresAt}`
388
+ )
389
+ return false
390
+ }
391
+
392
+ return acc.isActive === 'true' && acc.schedulable === 'true'
393
+ })
394
 
395
  if (availableAccounts.length === 0) {
396
  throw new Error('No available Azure OpenAI accounts')
src/services/bedrockAccountService.js CHANGED
@@ -6,19 +6,6 @@ const config = require('../../config/config')
6
  const bedrockRelayService = require('./bedrockRelayService')
7
  const LRUCache = require('../utils/lruCache')
8
 
9
- function normalizeSubscriptionExpiresAt(value) {
10
- if (value === undefined || value === null || value === '') {
11
- return ''
12
- }
13
-
14
- const date = value instanceof Date ? value : new Date(value)
15
- if (Number.isNaN(date.getTime())) {
16
- return ''
17
- }
18
-
19
- return date.toISOString()
20
- }
21
-
22
  class BedrockAccountService {
23
  constructor() {
24
  // 加密相关常量
@@ -53,8 +40,7 @@ class BedrockAccountService {
53
  accountType = 'shared', // 'dedicated' or 'shared'
54
  priority = 50, // 调度优先级 (1-100,数字越小优先级越高)
55
  schedulable = true, // 是否可被调度
56
- credentialType = 'default', // 'default', 'access_key', 'bearer_token'
57
- subscriptionExpiresAt = null
58
  } = options
59
 
60
  const accountId = uuidv4()
@@ -70,7 +56,11 @@ class BedrockAccountService {
70
  priority,
71
  schedulable,
72
  credentialType,
73
- subscriptionExpiresAt: normalizeSubscriptionExpiresAt(subscriptionExpiresAt),
 
 
 
 
74
  createdAt: new Date().toISOString(),
75
  updatedAt: new Date().toISOString(),
76
  type: 'bedrock' // 标识这是Bedrock账户
@@ -99,7 +89,6 @@ class BedrockAccountService {
99
  priority,
100
  schedulable,
101
  credentialType,
102
- subscriptionExpiresAt: accountData.subscriptionExpiresAt || null,
103
  createdAt: accountData.createdAt,
104
  type: 'bedrock'
105
  }
@@ -122,11 +111,6 @@ class BedrockAccountService {
122
  account.awsCredentials = this._decryptAwsCredentials(account.awsCredentials)
123
  }
124
 
125
- account.subscriptionExpiresAt =
126
- account.subscriptionExpiresAt && account.subscriptionExpiresAt !== ''
127
- ? account.subscriptionExpiresAt
128
- : null
129
-
130
  logger.debug(`🔍 获取Bedrock账户 - ID: ${accountId}, 名称: ${account.name}`)
131
 
132
  return {
@@ -163,12 +147,15 @@ class BedrockAccountService {
163
  priority: account.priority,
164
  schedulable: account.schedulable,
165
  credentialType: account.credentialType,
 
 
 
 
166
  createdAt: account.createdAt,
167
  updatedAt: account.updatedAt,
168
  type: 'bedrock',
169
- hasCredentials: !!account.awsCredentials,
170
- expiresAt: account.expiresAt || null,
171
- subscriptionExpiresAt: account.subscriptionExpiresAt || null
172
  })
173
  }
174
  }
@@ -234,14 +221,6 @@ class BedrockAccountService {
234
  account.credentialType = updates.credentialType
235
  }
236
 
237
- if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
238
- account.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(
239
- updates.subscriptionExpiresAt
240
- )
241
- } else if (Object.prototype.hasOwnProperty.call(updates, 'expiresAt')) {
242
- account.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.expiresAt)
243
- }
244
-
245
  // 更新AWS凭证
246
  if (updates.awsCredentials !== undefined) {
247
  if (updates.awsCredentials) {
@@ -256,6 +235,12 @@ class BedrockAccountService {
256
  logger.info(`🔐 重新加密Bedrock账户凭证 - ID: ${accountId}`)
257
  }
258
 
 
 
 
 
 
 
259
  account.updatedAt = new Date().toISOString()
260
 
261
  await client.set(`bedrock_account:${accountId}`, JSON.stringify(account))
@@ -276,9 +261,7 @@ class BedrockAccountService {
276
  schedulable: account.schedulable,
277
  credentialType: account.credentialType,
278
  updatedAt: account.updatedAt,
279
- type: 'bedrock',
280
- expiresAt: account.expiresAt || null,
281
- subscriptionExpiresAt: account.subscriptionExpiresAt || null
282
  }
283
  }
284
  } catch (error) {
@@ -315,9 +298,17 @@ class BedrockAccountService {
315
  return { success: false, error: 'Failed to get accounts' }
316
  }
317
 
318
- const availableAccounts = accountsResult.data.filter(
319
- (account) => account.isActive && account.schedulable
320
- )
 
 
 
 
 
 
 
 
321
 
322
  if (availableAccounts.length === 0) {
323
  return { success: false, error: 'No available Bedrock accounts' }
@@ -385,6 +376,19 @@ class BedrockAccountService {
385
  }
386
  }
387
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  // 🔑 生成加密密钥(缓存优化)
389
  _generateEncryptionKey() {
390
  if (!this._encryptionKeyCache) {
 
6
  const bedrockRelayService = require('./bedrockRelayService')
7
  const LRUCache = require('../utils/lruCache')
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  class BedrockAccountService {
10
  constructor() {
11
  // 加密相关常量
 
40
  accountType = 'shared', // 'dedicated' or 'shared'
41
  priority = 50, // 调度优先级 (1-100,数字越小优先级越高)
42
  schedulable = true, // 是否可被调度
43
+ credentialType = 'default' // 'default', 'access_key', 'bearer_token'
 
44
  } = options
45
 
46
  const accountId = uuidv4()
 
56
  priority,
57
  schedulable,
58
  credentialType,
59
+
60
+ // ✅ 新增:账户订阅到期时间(业务字段,手动管理)
61
+ // 注意:Bedrock 使用 AWS 凭证,没有 OAuth token,因此没有 expiresAt
62
+ subscriptionExpiresAt: options.subscriptionExpiresAt || null,
63
+
64
  createdAt: new Date().toISOString(),
65
  updatedAt: new Date().toISOString(),
66
  type: 'bedrock' // 标识这是Bedrock账户
 
89
  priority,
90
  schedulable,
91
  credentialType,
 
92
  createdAt: accountData.createdAt,
93
  type: 'bedrock'
94
  }
 
111
  account.awsCredentials = this._decryptAwsCredentials(account.awsCredentials)
112
  }
113
 
 
 
 
 
 
114
  logger.debug(`🔍 获取Bedrock账户 - ID: ${accountId}, 名称: ${account.name}`)
115
 
116
  return {
 
147
  priority: account.priority,
148
  schedulable: account.schedulable,
149
  credentialType: account.credentialType,
150
+
151
+ // ✅ 前端显示订阅过期时间(业务字段)
152
+ expiresAt: account.subscriptionExpiresAt || null,
153
+
154
  createdAt: account.createdAt,
155
  updatedAt: account.updatedAt,
156
  type: 'bedrock',
157
+ platform: 'bedrock',
158
+ hasCredentials: !!account.awsCredentials
 
159
  })
160
  }
161
  }
 
221
  account.credentialType = updates.credentialType
222
  }
223
 
 
 
 
 
 
 
 
 
224
  // 更新AWS凭证
225
  if (updates.awsCredentials !== undefined) {
226
  if (updates.awsCredentials) {
 
235
  logger.info(`🔐 重新加密Bedrock账户凭证 - ID: ${accountId}`)
236
  }
237
 
238
+ // ✅ 直接保存 subscriptionExpiresAt(如果提供)
239
+ // Bedrock 没有 token 刷新逻辑,不会覆盖此字段
240
+ if (updates.subscriptionExpiresAt !== undefined) {
241
+ account.subscriptionExpiresAt = updates.subscriptionExpiresAt
242
+ }
243
+
244
  account.updatedAt = new Date().toISOString()
245
 
246
  await client.set(`bedrock_account:${accountId}`, JSON.stringify(account))
 
261
  schedulable: account.schedulable,
262
  credentialType: account.credentialType,
263
  updatedAt: account.updatedAt,
264
+ type: 'bedrock'
 
 
265
  }
266
  }
267
  } catch (error) {
 
298
  return { success: false, error: 'Failed to get accounts' }
299
  }
300
 
301
+ const availableAccounts = accountsResult.data.filter((account) => {
302
+ // 检查账户订阅是否过期
303
+ if (this.isSubscriptionExpired(account)) {
304
+ logger.debug(
305
+ `⏰ Skipping expired Bedrock account: ${account.name}, expired at ${account.subscriptionExpiresAt || account.expiresAt}`
306
+ )
307
+ return false
308
+ }
309
+
310
+ return account.isActive && account.schedulable
311
+ })
312
 
313
  if (availableAccounts.length === 0) {
314
  return { success: false, error: 'No available Bedrock accounts' }
 
376
  }
377
  }
378
 
379
+ /**
380
+ * 检查账户订阅是否过期
381
+ * @param {Object} account - 账户对象
382
+ * @returns {boolean} - true: 已过期, false: 未过期
383
+ */
384
+ isSubscriptionExpired(account) {
385
+ if (!account.subscriptionExpiresAt) {
386
+ return false // 未设置视为永不过期
387
+ }
388
+ const expiryDate = new Date(account.subscriptionExpiresAt)
389
+ return expiryDate <= new Date()
390
+ }
391
+
392
  // 🔑 生成加密密钥(缓存优化)
393
  _generateEncryptionKey() {
394
  if (!this._encryptionKeyCache) {
src/services/ccrAccountService.js CHANGED
@@ -6,19 +6,6 @@ const logger = require('../utils/logger')
6
  const config = require('../../config/config')
7
  const LRUCache = require('../utils/lruCache')
8
 
9
- function normalizeSubscriptionExpiresAt(value) {
10
- if (value === undefined || value === null || value === '') {
11
- return ''
12
- }
13
-
14
- const date = value instanceof Date ? value : new Date(value)
15
- if (Number.isNaN(date.getTime())) {
16
- return ''
17
- }
18
-
19
- return date.toISOString()
20
- }
21
-
22
  class CcrAccountService {
23
  constructor() {
24
  // 加密相关常量
@@ -62,8 +49,7 @@ class CcrAccountService {
62
  accountType = 'shared', // 'dedicated' or 'shared'
63
  schedulable = true, // 是否可被调度
64
  dailyQuota = 0, // 每日额度限制(美元),0表示不限制
65
- quotaResetTime = '00:00', // 额度重置时间(HH:mm格式)
66
- subscriptionExpiresAt = null
67
  } = options
68
 
69
  // 验证必填字段
@@ -90,6 +76,11 @@ class CcrAccountService {
90
  proxy: proxy ? JSON.stringify(proxy) : '',
91
  isActive: isActive.toString(),
92
  accountType,
 
 
 
 
 
93
  createdAt: new Date().toISOString(),
94
  lastUsedAt: '',
95
  status: 'active',
@@ -105,8 +96,7 @@ class CcrAccountService {
105
  // 使用与统计一致的时区日期,避免边界问题
106
  lastResetDate: redis.getDateStringInTimezone(), // 最后重置日期(按配置时区)
107
  quotaResetTime, // 额度重置时间
108
- quotaStoppedAt: '', // 因额度停用的时间
109
- subscriptionExpiresAt: normalizeSubscriptionExpiresAt(subscriptionExpiresAt)
110
  }
111
 
112
  const client = redis.getClientSafe()
@@ -142,8 +132,7 @@ class CcrAccountService {
142
  dailyUsage: 0,
143
  lastResetDate: accountData.lastResetDate,
144
  quotaResetTime,
145
- quotaStoppedAt: null,
146
- subscriptionExpiresAt: accountData.subscriptionExpiresAt || null
147
  }
148
  }
149
 
@@ -181,14 +170,16 @@ class CcrAccountService {
181
  errorMessage: accountData.errorMessage,
182
  rateLimitInfo,
183
  schedulable: accountData.schedulable !== 'false', // 默认为true,只有明确设置为false才不可调度
 
 
 
 
184
  // 额度管理相关
185
  dailyQuota: parseFloat(accountData.dailyQuota || '0'),
186
  dailyUsage: parseFloat(accountData.dailyUsage || '0'),
187
  lastResetDate: accountData.lastResetDate || '',
188
  quotaResetTime: accountData.quotaResetTime || '00:00',
189
- quotaStoppedAt: accountData.quotaStoppedAt || null,
190
- expiresAt: accountData.expiresAt || null,
191
- subscriptionExpiresAt: accountData.subscriptionExpiresAt || null
192
  })
193
  }
194
  }
@@ -243,11 +234,6 @@ class CcrAccountService {
243
  `[DEBUG] Final CCR account data - name: ${accountData.name}, hasApiUrl: ${!!accountData.apiUrl}, hasApiKey: ${!!accountData.apiKey}, supportedModels: ${JSON.stringify(accountData.supportedModels)}`
244
  )
245
 
246
- accountData.subscriptionExpiresAt =
247
- accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== ''
248
- ? accountData.subscriptionExpiresAt
249
- : null
250
-
251
  return accountData
252
  }
253
 
@@ -311,12 +297,10 @@ class CcrAccountService {
311
  updatedData.quotaResetTime = updates.quotaResetTime
312
  }
313
 
314
- if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
315
- updatedData.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(
316
- updates.subscriptionExpiresAt
317
- )
318
- } else if (Object.prototype.hasOwnProperty.call(updates, 'expiresAt')) {
319
- updatedData.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.expiresAt)
320
  }
321
 
322
  await client.hset(`${this.ACCOUNT_KEY_PREFIX}${accountId}`, updatedData)
@@ -929,6 +913,19 @@ class CcrAccountService {
929
  throw error
930
  }
931
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
932
  }
933
 
934
  module.exports = new CcrAccountService()
 
6
  const config = require('../../config/config')
7
  const LRUCache = require('../utils/lruCache')
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  class CcrAccountService {
10
  constructor() {
11
  // 加密相关常量
 
49
  accountType = 'shared', // 'dedicated' or 'shared'
50
  schedulable = true, // 是否可被调度
51
  dailyQuota = 0, // 每日额度限制(美元),0表示不限制
52
+ quotaResetTime = '00:00' // 额度重置时间(HH:mm格式)
 
53
  } = options
54
 
55
  // 验证必填字段
 
76
  proxy: proxy ? JSON.stringify(proxy) : '',
77
  isActive: isActive.toString(),
78
  accountType,
79
+
80
+ // ✅ 新增:账户订阅到期时间(业务字段,手动管理)
81
+ // 注意:CCR 使用 API Key 认证,没有 OAuth token,因此没有 expiresAt
82
+ subscriptionExpiresAt: options.subscriptionExpiresAt || null,
83
+
84
  createdAt: new Date().toISOString(),
85
  lastUsedAt: '',
86
  status: 'active',
 
96
  // 使用与统计一致的时区日期,避免边界问题
97
  lastResetDate: redis.getDateStringInTimezone(), // 最后重置日期(按配置时区)
98
  quotaResetTime, // 额度重置时间
99
+ quotaStoppedAt: '' // 因额度停用的时间
 
100
  }
101
 
102
  const client = redis.getClientSafe()
 
132
  dailyUsage: 0,
133
  lastResetDate: accountData.lastResetDate,
134
  quotaResetTime,
135
+ quotaStoppedAt: null
 
136
  }
137
  }
138
 
 
170
  errorMessage: accountData.errorMessage,
171
  rateLimitInfo,
172
  schedulable: accountData.schedulable !== 'false', // 默认为true,只有明确设置为false才不可调度
173
+
174
+ // ✅ 前端显示订阅过期时间(业务字段)
175
+ expiresAt: accountData.subscriptionExpiresAt || null,
176
+
177
  // 额度管理相关
178
  dailyQuota: parseFloat(accountData.dailyQuota || '0'),
179
  dailyUsage: parseFloat(accountData.dailyUsage || '0'),
180
  lastResetDate: accountData.lastResetDate || '',
181
  quotaResetTime: accountData.quotaResetTime || '00:00',
182
+ quotaStoppedAt: accountData.quotaStoppedAt || null
 
 
183
  })
184
  }
185
  }
 
234
  `[DEBUG] Final CCR account data - name: ${accountData.name}, hasApiUrl: ${!!accountData.apiUrl}, hasApiKey: ${!!accountData.apiKey}, supportedModels: ${JSON.stringify(accountData.supportedModels)}`
235
  )
236
 
 
 
 
 
 
237
  return accountData
238
  }
239
 
 
297
  updatedData.quotaResetTime = updates.quotaResetTime
298
  }
299
 
300
+ // 直接保存 subscriptionExpiresAt(如果提供)
301
+ // CCR 使用 API Key,没有 token 刷新逻辑,不会覆盖此字段
302
+ if (updates.subscriptionExpiresAt !== undefined) {
303
+ updatedData.subscriptionExpiresAt = updates.subscriptionExpiresAt
 
 
304
  }
305
 
306
  await client.hset(`${this.ACCOUNT_KEY_PREFIX}${accountId}`, updatedData)
 
913
  throw error
914
  }
915
  }
916
+
917
+ /**
918
+ * ⏰ 检查账户订阅是否过期
919
+ * @param {Object} account - 账户对象
920
+ * @returns {boolean} - true: 已过期, false: 未过期
921
+ */
922
+ isSubscriptionExpired(account) {
923
+ if (!account.subscriptionExpiresAt) {
924
+ return false // 未设置视为永不过期
925
+ }
926
+ const expiryDate = new Date(account.subscriptionExpiresAt)
927
+ return expiryDate <= new Date()
928
+ }
929
  }
930
 
931
  module.exports = new CcrAccountService()
src/services/claudeAccountService.js CHANGED
@@ -787,13 +787,13 @@ class ClaudeAccountService {
787
  }
788
 
789
  /**
790
- * 检查账户是否未过期
791
  * @param {Object} account - 账户对象
792
- * @returns {boolean} - 如果未设置过期时间或未过期返回 true
793
  */
794
- isAccountNotExpired(account) {
795
  if (!account.subscriptionExpiresAt) {
796
- return true // 未设置过期时间,视为永不过期
797
  }
798
 
799
  const expiryDate = new Date(account.subscriptionExpiresAt)
@@ -803,10 +803,10 @@ class ClaudeAccountService {
803
  logger.debug(
804
  `⏰ Account ${account.name} (${account.id}) expired at ${account.subscriptionExpiresAt}`
805
  )
806
- return false
807
  }
808
 
809
- return true
810
  }
811
 
812
  // 🎯 智能选择可用账户(支持sticky会话和模型过滤)
@@ -819,7 +819,7 @@ class ClaudeAccountService {
819
  account.isActive === 'true' &&
820
  account.status !== 'error' &&
821
  account.schedulable !== 'false' &&
822
- this.isAccountNotExpired(account)
823
  )
824
 
825
  // 如果请求的是 Opus 模型,过滤掉 Pro 和 Free 账号
@@ -915,7 +915,7 @@ class ClaudeAccountService {
915
  boundAccount.isActive === 'true' &&
916
  boundAccount.status !== 'error' &&
917
  boundAccount.schedulable !== 'false' &&
918
- this.isAccountNotExpired(boundAccount)
919
  ) {
920
  logger.info(
921
  `🎯 Using bound dedicated account: ${boundAccount.name} (${apiKeyData.claudeAccountId}) for API key ${apiKeyData.name}`
@@ -937,7 +937,7 @@ class ClaudeAccountService {
937
  account.status !== 'error' &&
938
  account.schedulable !== 'false' &&
939
  (account.accountType === 'shared' || !account.accountType) && // 兼容旧数据
940
- this.isAccountNotExpired(account)
941
  )
942
 
943
  // 如果请求的是 Opus 模型,过滤掉 Pro 和 Free 账号
 
787
  }
788
 
789
  /**
790
+ * 检查账户订阅是否过期
791
  * @param {Object} account - 账户对象
792
+ * @returns {boolean} - true: 已过期, false: 未过期
793
  */
794
+ isSubscriptionExpired(account) {
795
  if (!account.subscriptionExpiresAt) {
796
+ return false // 未设置过期时间,视为永不过期
797
  }
798
 
799
  const expiryDate = new Date(account.subscriptionExpiresAt)
 
803
  logger.debug(
804
  `⏰ Account ${account.name} (${account.id}) expired at ${account.subscriptionExpiresAt}`
805
  )
806
+ return true
807
  }
808
 
809
+ return false
810
  }
811
 
812
  // 🎯 智能选择可用账户(支持sticky会话和模型过滤)
 
819
  account.isActive === 'true' &&
820
  account.status !== 'error' &&
821
  account.schedulable !== 'false' &&
822
+ !this.isSubscriptionExpired(account)
823
  )
824
 
825
  // 如果请求的是 Opus 模型,过滤掉 Pro 和 Free 账号
 
915
  boundAccount.isActive === 'true' &&
916
  boundAccount.status !== 'error' &&
917
  boundAccount.schedulable !== 'false' &&
918
+ !this.isSubscriptionExpired(boundAccount)
919
  ) {
920
  logger.info(
921
  `🎯 Using bound dedicated account: ${boundAccount.name} (${apiKeyData.claudeAccountId}) for API key ${apiKeyData.name}`
 
937
  account.status !== 'error' &&
938
  account.schedulable !== 'false' &&
939
  (account.accountType === 'shared' || !account.accountType) && // 兼容旧数据
940
+ !this.isSubscriptionExpired(account)
941
  )
942
 
943
  // 如果请求的是 Opus 模型,过滤掉 Pro 和 Free 账号
src/services/claudeConsoleAccountService.js CHANGED
@@ -6,19 +6,6 @@ const logger = require('../utils/logger')
6
  const config = require('../../config/config')
7
  const LRUCache = require('../utils/lruCache')
8
 
9
- function normalizeSubscriptionExpiresAt(value) {
10
- if (value === undefined || value === null || value === '') {
11
- return ''
12
- }
13
-
14
- const date = value instanceof Date ? value : new Date(value)
15
- if (Number.isNaN(date.getTime())) {
16
- return ''
17
- }
18
-
19
- return date.toISOString()
20
- }
21
-
22
  class ClaudeConsoleAccountService {
23
  constructor() {
24
  // 加密相关常量
@@ -65,8 +52,7 @@ class ClaudeConsoleAccountService {
65
  accountType = 'shared', // 'dedicated' or 'shared'
66
  schedulable = true, // 是否可被调度
67
  dailyQuota = 0, // 每日额度限制(美元),0表示不限制
68
- quotaResetTime = '00:00', // 额度重置时间(HH:mm格式)
69
- subscriptionExpiresAt = null
70
  } = options
71
 
72
  // 验证必填字段
@@ -97,6 +83,11 @@ class ClaudeConsoleAccountService {
97
  lastUsedAt: '',
98
  status: 'active',
99
  errorMessage: '',
 
 
 
 
 
100
  // 限流相关
101
  rateLimitedAt: '',
102
  rateLimitStatus: '',
@@ -108,8 +99,7 @@ class ClaudeConsoleAccountService {
108
  // 使用与统计一致的时区日期,避免边界问题
109
  lastResetDate: redis.getDateStringInTimezone(), // 最后重置日期(按配置时区)
110
  quotaResetTime, // 额度重置时间
111
- quotaStoppedAt: '', // 因额度停用的时间
112
- subscriptionExpiresAt: normalizeSubscriptionExpiresAt(subscriptionExpiresAt)
113
  }
114
 
115
  const client = redis.getClientSafe()
@@ -145,8 +135,7 @@ class ClaudeConsoleAccountService {
145
  dailyUsage: 0,
146
  lastResetDate: accountData.lastResetDate,
147
  quotaResetTime,
148
- quotaStoppedAt: null,
149
- subscriptionExpiresAt: accountData.subscriptionExpiresAt || null
150
  }
151
  }
152
 
@@ -184,14 +173,16 @@ class ClaudeConsoleAccountService {
184
  errorMessage: accountData.errorMessage,
185
  rateLimitInfo,
186
  schedulable: accountData.schedulable !== 'false', // 默认为true,只有明确设置为false才不可调度
 
 
 
 
187
  // 额度管理相关
188
  dailyQuota: parseFloat(accountData.dailyQuota || '0'),
189
  dailyUsage: parseFloat(accountData.dailyUsage || '0'),
190
  lastResetDate: accountData.lastResetDate || '',
191
  quotaResetTime: accountData.quotaResetTime || '00:00',
192
- quotaStoppedAt: accountData.quotaStoppedAt || null,
193
- expiresAt: accountData.expiresAt || null,
194
- subscriptionExpiresAt: accountData.subscriptionExpiresAt || null
195
  })
196
  }
197
  }
@@ -242,11 +233,6 @@ class ClaudeConsoleAccountService {
242
  accountData.proxy = JSON.parse(accountData.proxy)
243
  }
244
 
245
- accountData.subscriptionExpiresAt =
246
- accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== ''
247
- ? accountData.subscriptionExpiresAt
248
- : null
249
-
250
  logger.debug(
251
  `[DEBUG] Final account data - name: ${accountData.name}, hasApiUrl: ${!!accountData.apiUrl}, hasApiKey: ${!!accountData.apiKey}, supportedModels: ${JSON.stringify(accountData.supportedModels)}`
252
  )
@@ -341,12 +327,10 @@ class ClaudeConsoleAccountService {
341
  updatedData.quotaStoppedAt = updates.quotaStoppedAt
342
  }
343
 
344
- if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
345
- updatedData.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(
346
- updates.subscriptionExpiresAt
347
- )
348
- } else if (Object.prototype.hasOwnProperty.call(updates, 'expiresAt')) {
349
- updatedData.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.expiresAt)
350
  }
351
 
352
  // 处理账户类型变更
@@ -1270,6 +1254,19 @@ class ClaudeConsoleAccountService {
1270
  throw error
1271
  }
1272
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
1273
  }
1274
 
1275
  module.exports = new ClaudeConsoleAccountService()
 
6
  const config = require('../../config/config')
7
  const LRUCache = require('../utils/lruCache')
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  class ClaudeConsoleAccountService {
10
  constructor() {
11
  // 加密相关常量
 
52
  accountType = 'shared', // 'dedicated' or 'shared'
53
  schedulable = true, // 是否可被调度
54
  dailyQuota = 0, // 每日额度限制(美元),0表示不限制
55
+ quotaResetTime = '00:00' // 额度重置时间(HH:mm格式)
 
56
  } = options
57
 
58
  // 验证必填字段
 
83
  lastUsedAt: '',
84
  status: 'active',
85
  errorMessage: '',
86
+
87
+ // ✅ 新增:账户订阅到期时间(业务字段,手动管理)
88
+ // 注意:Claude Console 没有 OAuth token,因此没有 expiresAt(token过期)
89
+ subscriptionExpiresAt: options.subscriptionExpiresAt || null,
90
+
91
  // 限流相关
92
  rateLimitedAt: '',
93
  rateLimitStatus: '',
 
99
  // 使用与统计一致的时区日期,避免边界问题
100
  lastResetDate: redis.getDateStringInTimezone(), // 最后重置日期(按配置时区)
101
  quotaResetTime, // 额度重置时间
102
+ quotaStoppedAt: '' // 因额度停用的时间
 
103
  }
104
 
105
  const client = redis.getClientSafe()
 
135
  dailyUsage: 0,
136
  lastResetDate: accountData.lastResetDate,
137
  quotaResetTime,
138
+ quotaStoppedAt: null
 
139
  }
140
  }
141
 
 
173
  errorMessage: accountData.errorMessage,
174
  rateLimitInfo,
175
  schedulable: accountData.schedulable !== 'false', // 默认为true,只有明确设置为false才不可调度
176
+
177
+ // ✅ 前端显示订阅过期时间(业务字段)
178
+ expiresAt: accountData.subscriptionExpiresAt || null,
179
+
180
  // 额度管理相关
181
  dailyQuota: parseFloat(accountData.dailyQuota || '0'),
182
  dailyUsage: parseFloat(accountData.dailyUsage || '0'),
183
  lastResetDate: accountData.lastResetDate || '',
184
  quotaResetTime: accountData.quotaResetTime || '00:00',
185
+ quotaStoppedAt: accountData.quotaStoppedAt || null
 
 
186
  })
187
  }
188
  }
 
233
  accountData.proxy = JSON.parse(accountData.proxy)
234
  }
235
 
 
 
 
 
 
236
  logger.debug(
237
  `[DEBUG] Final account data - name: ${accountData.name}, hasApiUrl: ${!!accountData.apiUrl}, hasApiKey: ${!!accountData.apiKey}, supportedModels: ${JSON.stringify(accountData.supportedModels)}`
238
  )
 
327
  updatedData.quotaStoppedAt = updates.quotaStoppedAt
328
  }
329
 
330
+ // 直接保存 subscriptionExpiresAt(如果提供)
331
+ // Claude Console 没有 token 刷新逻辑,不会覆盖此字段
332
+ if (updates.subscriptionExpiresAt !== undefined) {
333
+ updatedData.subscriptionExpiresAt = updates.subscriptionExpiresAt
 
 
334
  }
335
 
336
  // 处理账户类型变更
 
1254
  throw error
1255
  }
1256
  }
1257
+
1258
+ /**
1259
+ * ⏰ 检查账户订阅是否过期
1260
+ * @param {Object} account - 账户对象
1261
+ * @returns {boolean} - true: 已过期, false: 未过期
1262
+ */
1263
+ isSubscriptionExpired(account) {
1264
+ if (!account.subscriptionExpiresAt) {
1265
+ return false // 未设置视为永不过期
1266
+ }
1267
+ const expiryDate = new Date(account.subscriptionExpiresAt)
1268
+ return expiryDate <= new Date()
1269
+ }
1270
  }
1271
 
1272
  module.exports = new ClaudeConsoleAccountService()
src/services/droidAccountService.js CHANGED
@@ -794,7 +794,11 @@ class DroidAccountService {
794
  description,
795
  refreshToken: this._encryptSensitiveData(normalizedRefreshToken),
796
  accessToken: this._encryptSensitiveData(normalizedAccessToken),
797
- expiresAt: normalizedExpiresAt || '',
 
 
 
 
798
  proxy: proxy ? JSON.stringify(proxy) : '',
799
  isActive: isActive.toString(),
800
  accountType,
@@ -880,6 +884,11 @@ class DroidAccountService {
880
  accessToken: account.accessToken
881
  ? maskToken(this._decryptSensitiveData(account.accessToken))
882
  : '',
 
 
 
 
 
883
  apiKeyCount: (() => {
884
  const parsedCount = this._parseApiKeyEntries(account.apiKeys).length
885
  if (account.apiKeyCount === undefined || account.apiKeyCount === null) {
@@ -1020,6 +1029,12 @@ class DroidAccountService {
1020
  }
1021
  }
1022
 
 
 
 
 
 
 
1023
  if (sanitizedUpdates.proxy === undefined) {
1024
  sanitizedUpdates.proxy = account.proxy || ''
1025
  }
@@ -1374,6 +1389,19 @@ class DroidAccountService {
1374
  return hoursSinceRefresh >= this.refreshIntervalHours
1375
  }
1376
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1377
  /**
1378
  * 获取有效的 access token(自动刷新)
1379
  */
@@ -1419,6 +1447,14 @@ class DroidAccountService {
1419
  const isSchedulable = this._isTruthy(account.schedulable)
1420
  const status = typeof account.status === 'string' ? account.status.toLowerCase() : ''
1421
 
 
 
 
 
 
 
 
 
1422
  if (!isActive || !isSchedulable || status !== 'active') {
1423
  return false
1424
  }
 
794
  description,
795
  refreshToken: this._encryptSensitiveData(normalizedRefreshToken),
796
  accessToken: this._encryptSensitiveData(normalizedAccessToken),
797
+ expiresAt: normalizedExpiresAt || '', // OAuth Token 过期时间(技术字段,自动刷新)
798
+
799
+ // ✅ 新增:账户订阅到期时间(业务字段,手动管理)
800
+ subscriptionExpiresAt: options.subscriptionExpiresAt || null,
801
+
802
  proxy: proxy ? JSON.stringify(proxy) : '',
803
  isActive: isActive.toString(),
804
  accountType,
 
884
  accessToken: account.accessToken
885
  ? maskToken(this._decryptSensitiveData(account.accessToken))
886
  : '',
887
+
888
+ // ✅ 前端显示订阅过期时间(业务字段)
889
+ expiresAt: account.subscriptionExpiresAt || null,
890
+ platform: account.platform || 'droid',
891
+
892
  apiKeyCount: (() => {
893
  const parsedCount = this._parseApiKeyEntries(account.apiKeys).length
894
  if (account.apiKeyCount === undefined || account.apiKeyCount === null) {
 
1029
  }
1030
  }
1031
 
1032
+ // ✅ 如果通过路由映射更新了 subscriptionExpiresAt,直接保存
1033
+ // subscriptionExpiresAt 是业务字段,与 token 刷新独立
1034
+ if (sanitizedUpdates.subscriptionExpiresAt !== undefined) {
1035
+ // 直接保存,不做任何调整
1036
+ }
1037
+
1038
  if (sanitizedUpdates.proxy === undefined) {
1039
  sanitizedUpdates.proxy = account.proxy || ''
1040
  }
 
1389
  return hoursSinceRefresh >= this.refreshIntervalHours
1390
  }
1391
 
1392
+ /**
1393
+ * 检查账户订阅是否过期
1394
+ * @param {Object} account - 账户对象
1395
+ * @returns {boolean} - true: 已过期, false: 未过期
1396
+ */
1397
+ isSubscriptionExpired(account) {
1398
+ if (!account.subscriptionExpiresAt) {
1399
+ return false // 未设置视为永不过期
1400
+ }
1401
+ const expiryDate = new Date(account.subscriptionExpiresAt)
1402
+ return expiryDate <= new Date()
1403
+ }
1404
+
1405
  /**
1406
  * 获取有效的 access token(自动刷新)
1407
  */
 
1447
  const isSchedulable = this._isTruthy(account.schedulable)
1448
  const status = typeof account.status === 'string' ? account.status.toLowerCase() : ''
1449
 
1450
+ // ✅ 检查账户订阅是否过期
1451
+ if (this.isSubscriptionExpired(account)) {
1452
+ logger.debug(
1453
+ `⏰ Skipping expired Droid account: ${account.name}, expired at ${account.subscriptionExpiresAt}`
1454
+ )
1455
+ return false
1456
+ }
1457
+
1458
  if (!isActive || !isSchedulable || status !== 'active') {
1459
  return false
1460
  }
src/services/droidRelayService.js CHANGED
@@ -7,8 +7,10 @@ const apiKeyService = require('./apiKeyService')
7
  const redis = require('../models/redis')
8
  const { updateRateLimitCounters } = require('../utils/rateLimitHelper')
9
  const logger = require('../utils/logger')
 
10
 
11
  const SYSTEM_PROMPT = 'You are Droid, an AI software engineering agent built by Factory.'
 
12
 
13
  /**
14
  * Droid API 转发服务
@@ -23,7 +25,7 @@ class DroidRelayService {
23
  openai: '/o/v1/responses'
24
  }
25
 
26
- this.userAgent = 'factory-cli/0.19.4'
27
  this.systemPrompt = SYSTEM_PROMPT
28
  this.API_KEY_STICKY_PREFIX = 'droid_api_key'
29
  }
@@ -246,11 +248,34 @@ class DroidRelayService {
246
  // 处理请求体(注入 system prompt 等)
247
  const streamRequested = !disableStreaming && this._isStreamRequested(normalizedRequestBody)
248
 
249
- const processedBody = this._processRequestBody(normalizedRequestBody, normalizedEndpoint, {
250
  disableStreaming,
251
  streamRequested
252
  })
253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  // 发送请求
255
  const isStreaming = streamRequested
256
 
 
7
  const redis = require('../models/redis')
8
  const { updateRateLimitCounters } = require('../utils/rateLimitHelper')
9
  const logger = require('../utils/logger')
10
+ const runtimeAddon = require('../utils/runtimeAddon')
11
 
12
  const SYSTEM_PROMPT = 'You are Droid, an AI software engineering agent built by Factory.'
13
+ const RUNTIME_EVENT_FMT_PAYLOAD = 'fmtPayload'
14
 
15
  /**
16
  * Droid API 转发服务
 
25
  openai: '/o/v1/responses'
26
  }
27
 
28
+ this.userAgent = 'factory-cli/0.19.12'
29
  this.systemPrompt = SYSTEM_PROMPT
30
  this.API_KEY_STICKY_PREFIX = 'droid_api_key'
31
  }
 
248
  // 处理请求体(注入 system prompt 等)
249
  const streamRequested = !disableStreaming && this._isStreamRequested(normalizedRequestBody)
250
 
251
+ let processedBody = this._processRequestBody(normalizedRequestBody, normalizedEndpoint, {
252
  disableStreaming,
253
  streamRequested
254
  })
255
 
256
+ const extensionPayload = {
257
+ body: processedBody,
258
+ endpoint: normalizedEndpoint,
259
+ rawRequest: normalizedRequestBody,
260
+ originalRequest: requestBody
261
+ }
262
+
263
+ const extensionResult = runtimeAddon.emitSync(RUNTIME_EVENT_FMT_PAYLOAD, extensionPayload)
264
+ const resolvedPayload =
265
+ extensionResult && typeof extensionResult === 'object' ? extensionResult : extensionPayload
266
+
267
+ if (resolvedPayload && typeof resolvedPayload === 'object') {
268
+ if (resolvedPayload.abortResponse && typeof resolvedPayload.abortResponse === 'object') {
269
+ return resolvedPayload.abortResponse
270
+ }
271
+
272
+ if (resolvedPayload.body && typeof resolvedPayload.body === 'object') {
273
+ processedBody = resolvedPayload.body
274
+ } else if (resolvedPayload !== extensionPayload) {
275
+ processedBody = resolvedPayload
276
+ }
277
+ }
278
+
279
  // 发送请求
280
  const isStreaming = streamRequested
281
 
src/services/droidScheduler.js CHANGED
@@ -171,7 +171,7 @@ class DroidScheduler {
171
 
172
  if (filtered.length === 0) {
173
  throw new Error(
174
- `No available Droid accounts for endpoint ${normalizedEndpoint}${apiKeyData?.droidAccountId ? ' (respecting binding)' : ''}`
175
  )
176
  }
177
 
@@ -196,9 +196,7 @@ class DroidScheduler {
196
  const selected = sorted[0]
197
 
198
  if (!selected) {
199
- throw new Error(
200
- `No schedulable Droid account available after sorting (${normalizedEndpoint})`
201
- )
202
  }
203
 
204
  if (stickyKey && !isDedicatedBinding) {
 
171
 
172
  if (filtered.length === 0) {
173
  throw new Error(
174
+ `No available accounts for endpoint ${normalizedEndpoint}${apiKeyData?.droidAccountId ? ' (respecting binding)' : ''}`
175
  )
176
  }
177
 
 
196
  const selected = sorted[0]
197
 
198
  if (!selected) {
199
+ throw new Error(`No schedulable account available after sorting (${normalizedEndpoint})`)
 
 
200
  }
201
 
202
  if (stickyKey && !isDedicatedBinding) {
src/services/geminiAccountService.js CHANGED
@@ -42,19 +42,6 @@ function generateEncryptionKey() {
42
  return _encryptionKeyCache
43
  }
44
 
45
- function normalizeSubscriptionExpiresAt(value) {
46
- if (value === undefined || value === null || value === '') {
47
- return ''
48
- }
49
-
50
- const date = value instanceof Date ? value : new Date(value)
51
- if (Number.isNaN(date.getTime())) {
52
- return ''
53
- }
54
-
55
- return date.toISOString()
56
- }
57
-
58
  // Gemini 账户键前缀
59
  const GEMINI_ACCOUNT_KEY_PREFIX = 'gemini_account:'
60
  const SHARED_GEMINI_ACCOUNTS_KEY = 'shared_gemini_accounts'
@@ -346,10 +333,6 @@ async function createAccount(accountData) {
346
  let refreshToken = ''
347
  let expiresAt = ''
348
 
349
- const subscriptionExpiresAt = normalizeSubscriptionExpiresAt(
350
- accountData.subscriptionExpiresAt || ''
351
- )
352
-
353
  if (accountData.geminiOauth || accountData.accessToken) {
354
  // 如果提供了完整的 OAuth 数据
355
  if (accountData.geminiOauth) {
@@ -401,10 +384,13 @@ async function createAccount(accountData) {
401
  geminiOauth: geminiOauth ? encrypt(geminiOauth) : '',
402
  accessToken: accessToken ? encrypt(accessToken) : '',
403
  refreshToken: refreshToken ? encrypt(refreshToken) : '',
404
- expiresAt,
405
  // 只有OAuth方式才有scopes,手动添加的没有
406
  scopes: accountData.geminiOauth ? accountData.scopes || OAUTH_SCOPES.join(' ') : '',
407
 
 
 
 
408
  // 代理设置
409
  proxy: accountData.proxy ? JSON.stringify(accountData.proxy) : '',
410
 
@@ -421,8 +407,7 @@ async function createAccount(accountData) {
421
  createdAt: now,
422
  updatedAt: now,
423
  lastUsedAt: '',
424
- lastRefreshAt: '',
425
- subscriptionExpiresAt
426
  }
427
 
428
  // 保存到 Redis
@@ -446,10 +431,6 @@ async function createAccount(accountData) {
446
  }
447
  }
448
 
449
- if (!returnAccount.subscriptionExpiresAt) {
450
- returnAccount.subscriptionExpiresAt = null
451
- }
452
-
453
  return returnAccount
454
  }
455
 
@@ -486,10 +467,6 @@ async function getAccount(accountId) {
486
  // 转换 schedulable 字符串为布尔值(与 claudeConsoleAccountService 保持一致)
487
  accountData.schedulable = accountData.schedulable !== 'false' // 默认为true,只有明确设置为'false'才为false
488
 
489
- if (!accountData.subscriptionExpiresAt) {
490
- accountData.subscriptionExpiresAt = null
491
- }
492
-
493
  return accountData
494
  }
495
 
@@ -503,10 +480,6 @@ async function updateAccount(accountId, updates) {
503
  const now = new Date().toISOString()
504
  updates.updatedAt = now
505
 
506
- if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
507
- updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.subscriptionExpiresAt)
508
- }
509
-
510
  // 检查是否新增了 refresh token
511
  // existingAccount.refreshToken 已经是解密后的值了(从 getAccount 返回)
512
  const oldRefreshToken = existingAccount.refreshToken || ''
@@ -551,15 +524,23 @@ async function updateAccount(accountId, updates) {
551
  }
552
  }
553
 
554
- // 如果新增了 refresh token,更新过期时间为10分钟
 
555
  if (needUpdateExpiry) {
556
  const newExpiry = new Date(Date.now() + 10 * 60 * 1000).toISOString()
557
- updates.expiresAt = newExpiry
 
558
  logger.info(
559
- `🔄 New refresh token added for Gemini account ${accountId}, setting expiry to 10 minutes`
560
  )
561
  }
562
 
 
 
 
 
 
 
563
  // 如果通过 geminiOauth 更新,也要检查是否新增了 refresh token
564
  if (updates.geminiOauth && !oldRefreshToken) {
565
  const oauthData =
@@ -616,10 +597,6 @@ async function updateAccount(accountId, updates) {
616
  }
617
  }
618
 
619
- if (!updatedAccount.subscriptionExpiresAt) {
620
- updatedAccount.subscriptionExpiresAt = null
621
- }
622
-
623
  return updatedAccount
624
  }
625
 
@@ -677,13 +654,25 @@ async function getAllAccounts() {
677
  // 转换 schedulable 字符串为布尔值(与 getAccount 保持一致)
678
  accountData.schedulable = accountData.schedulable !== 'false' // 默认为true,只有明确设置为'false'才为false
679
 
 
 
 
 
 
 
680
  // 不解密敏感字段,只返回基本信息
681
  accounts.push({
682
  ...accountData,
683
  geminiOauth: accountData.geminiOauth ? '[ENCRYPTED]' : '',
684
  accessToken: accountData.accessToken ? '[ENCRYPTED]' : '',
685
  refreshToken: accountData.refreshToken ? '[ENCRYPTED]' : '',
686
- subscriptionExpiresAt: accountData.subscriptionExpiresAt || null,
 
 
 
 
 
 
687
  // 添加 scopes 字段用于判断认证方式
688
  // 处理空字符串和默认值的情况
689
  scopes:
@@ -762,8 +751,17 @@ async function selectAvailableAccount(apiKeyId, sessionHash = null) {
762
 
763
  for (const accountId of sharedAccountIds) {
764
  const account = await getAccount(accountId)
765
- if (account && account.isActive === 'true' && !isRateLimited(account)) {
 
 
 
 
 
766
  availableAccounts.push(account)
 
 
 
 
767
  }
768
  }
769
 
@@ -818,6 +816,19 @@ function isTokenExpired(account) {
818
  return now >= expiryTime - buffer
819
  }
820
 
 
 
 
 
 
 
 
 
 
 
 
 
 
821
  // 检查账户是否被限流
822
  function isRateLimited(account) {
823
  if (account.rateLimitStatus === 'limited' && account.rateLimitedAt) {
 
42
  return _encryptionKeyCache
43
  }
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  // Gemini 账户键前缀
46
  const GEMINI_ACCOUNT_KEY_PREFIX = 'gemini_account:'
47
  const SHARED_GEMINI_ACCOUNTS_KEY = 'shared_gemini_accounts'
 
333
  let refreshToken = ''
334
  let expiresAt = ''
335
 
 
 
 
 
336
  if (accountData.geminiOauth || accountData.accessToken) {
337
  // 如果提供了完整的 OAuth 数据
338
  if (accountData.geminiOauth) {
 
384
  geminiOauth: geminiOauth ? encrypt(geminiOauth) : '',
385
  accessToken: accessToken ? encrypt(accessToken) : '',
386
  refreshToken: refreshToken ? encrypt(refreshToken) : '',
387
+ expiresAt, // OAuth Token 过期时间(技术字段,自动刷新)
388
  // 只有OAuth方式才有scopes,手动添加的没有
389
  scopes: accountData.geminiOauth ? accountData.scopes || OAUTH_SCOPES.join(' ') : '',
390
 
391
+ // ✅ 新增:账户订阅到期时间(业务字段,手动管理)
392
+ subscriptionExpiresAt: accountData.subscriptionExpiresAt || null,
393
+
394
  // 代理设置
395
  proxy: accountData.proxy ? JSON.stringify(accountData.proxy) : '',
396
 
 
407
  createdAt: now,
408
  updatedAt: now,
409
  lastUsedAt: '',
410
+ lastRefreshAt: ''
 
411
  }
412
 
413
  // 保存到 Redis
 
431
  }
432
  }
433
 
 
 
 
 
434
  return returnAccount
435
  }
436
 
 
467
  // 转换 schedulable 字符串为布尔值(与 claudeConsoleAccountService 保持一致)
468
  accountData.schedulable = accountData.schedulable !== 'false' // 默认为true,只有明确设置为'false'才为false
469
 
 
 
 
 
470
  return accountData
471
  }
472
 
 
480
  const now = new Date().toISOString()
481
  updates.updatedAt = now
482
 
 
 
 
 
483
  // 检查是否新增了 refresh token
484
  // existingAccount.refreshToken 已经是解密后的值了(从 getAccount 返回)
485
  const oldRefreshToken = existingAccount.refreshToken || ''
 
524
  }
525
  }
526
 
527
+ // 关键:如果新增了 refresh token,只更新 token 过期时间
528
+ // 不要覆盖 subscriptionExpiresAt
529
  if (needUpdateExpiry) {
530
  const newExpiry = new Date(Date.now() + 10 * 60 * 1000).toISOString()
531
+ updates.expiresAt = newExpiry // 只更新 OAuth Token 过期时间
532
+ // ⚠️ 重要:不要修改 subscriptionExpiresAt
533
  logger.info(
534
+ `🔄 New refresh token added for Gemini account ${accountId}, setting token expiry to 10 minutes`
535
  )
536
  }
537
 
538
+ // ✅ 如果通过路由映射更新了 subscriptionExpiresAt,直接保存
539
+ // subscriptionExpiresAt 是业务字段,与 token 刷新独立
540
+ if (updates.subscriptionExpiresAt !== undefined) {
541
+ // 直接保存,不做任何调整
542
+ }
543
+
544
  // 如果通过 geminiOauth 更新,也要检查是否新增了 refresh token
545
  if (updates.geminiOauth && !oldRefreshToken) {
546
  const oauthData =
 
597
  }
598
  }
599
 
 
 
 
 
600
  return updatedAccount
601
  }
602
 
 
654
  // 转换 schedulable 字符串为布尔值(与 getAccount 保持一致)
655
  accountData.schedulable = accountData.schedulable !== 'false' // 默认为true,只有明确设置为'false'才为false
656
 
657
+ const tokenExpiresAt = accountData.expiresAt || null
658
+ const subscriptionExpiresAt =
659
+ accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== ''
660
+ ? accountData.subscriptionExpiresAt
661
+ : null
662
+
663
  // 不解密敏感字段,只返回基本信息
664
  accounts.push({
665
  ...accountData,
666
  geminiOauth: accountData.geminiOauth ? '[ENCRYPTED]' : '',
667
  accessToken: accountData.accessToken ? '[ENCRYPTED]' : '',
668
  refreshToken: accountData.refreshToken ? '[ENCRYPTED]' : '',
669
+
670
+ // ✅ 前端显示订阅过期时间(业务字段)
671
+ // 注意:前端看到的 expiresAt 实际上是 subscriptionExpiresAt
672
+ tokenExpiresAt,
673
+ subscriptionExpiresAt,
674
+ expiresAt: subscriptionExpiresAt,
675
+
676
  // 添加 scopes 字段用于判断认证方式
677
  // 处理空字符串和默认值的情况
678
  scopes:
 
751
 
752
  for (const accountId of sharedAccountIds) {
753
  const account = await getAccount(accountId)
754
+ if (
755
+ account &&
756
+ account.isActive === 'true' &&
757
+ !isRateLimited(account) &&
758
+ !isSubscriptionExpired(account)
759
+ ) {
760
  availableAccounts.push(account)
761
+ } else if (account && isSubscriptionExpired(account)) {
762
+ logger.debug(
763
+ `⏰ Skipping expired Gemini account: ${account.name}, expired at ${account.subscriptionExpiresAt}`
764
+ )
765
  }
766
  }
767
 
 
816
  return now >= expiryTime - buffer
817
  }
818
 
819
+ /**
820
+ * 检查账户订阅是否过期
821
+ * @param {Object} account - 账户对象
822
+ * @returns {boolean} - true: 已过期, false: 未过期
823
+ */
824
+ function isSubscriptionExpired(account) {
825
+ if (!account.subscriptionExpiresAt) {
826
+ return false // 未设置视为永不过期
827
+ }
828
+ const expiryDate = new Date(account.subscriptionExpiresAt)
829
+ return expiryDate <= new Date()
830
+ }
831
+
832
  // 检查账户是否被限流
833
  function isRateLimited(account) {
834
  if (account.rateLimitStatus === 'limited' && account.rateLimitedAt) {
src/services/modelService.js ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const logger = require('../utils/logger')
4
+
5
+ /**
6
+ * 模型服务
7
+ * 管理系统支持的 AI 模型列表
8
+ * 与 pricingService 独立,专注于"支持哪些模型"而不是"如何计费"
9
+ */
10
+ class ModelService {
11
+ constructor() {
12
+ this.modelsFile = path.join(process.cwd(), 'data', 'supported_models.json')
13
+ this.supportedModels = null
14
+ this.fileWatcher = null
15
+ }
16
+
17
+ /**
18
+ * 初始化模型服务
19
+ */
20
+ async initialize() {
21
+ try {
22
+ this.loadModels()
23
+ this.setupFileWatcher()
24
+ logger.success('✅ Model service initialized successfully')
25
+ } catch (error) {
26
+ logger.error('❌ Failed to initialize model service:', error)
27
+ }
28
+ }
29
+
30
+ /**
31
+ * 加载支持的模型配置
32
+ */
33
+ loadModels() {
34
+ try {
35
+ if (fs.existsSync(this.modelsFile)) {
36
+ const data = fs.readFileSync(this.modelsFile, 'utf8')
37
+ this.supportedModels = JSON.parse(data)
38
+
39
+ const totalModels = Object.values(this.supportedModels).reduce(
40
+ (sum, config) => sum + config.models.length,
41
+ 0
42
+ )
43
+
44
+ logger.info(`📋 Loaded ${totalModels} supported models from configuration`)
45
+ } else {
46
+ logger.warn('⚠️ Supported models file not found, using defaults')
47
+ this.supportedModels = this.getDefaultModels()
48
+
49
+ // 创建默认配置文件
50
+ this.saveDefaultConfig()
51
+ }
52
+ } catch (error) {
53
+ logger.error('❌ Failed to load supported models:', error)
54
+ this.supportedModels = this.getDefaultModels()
55
+ }
56
+ }
57
+
58
+ /**
59
+ * 获取默认模型配置(后备方案)
60
+ */
61
+ getDefaultModels() {
62
+ return {
63
+ claude: {
64
+ provider: 'anthropic',
65
+ description: 'Claude models from Anthropic',
66
+ models: [
67
+ 'claude-sonnet-4-5-20250929',
68
+ 'claude-opus-4-1-20250805',
69
+ 'claude-sonnet-4-20250514',
70
+ 'claude-opus-4-20250514',
71
+ 'claude-3-7-sonnet-20250219',
72
+ 'claude-3-5-sonnet-20241022',
73
+ 'claude-3-5-haiku-20241022',
74
+ 'claude-3-opus-20240229',
75
+ 'claude-3-haiku-20240307'
76
+ ]
77
+ },
78
+ openai: {
79
+ provider: 'openai',
80
+ description: 'OpenAI GPT models',
81
+ models: [
82
+ 'gpt-4o',
83
+ 'gpt-4o-mini',
84
+ 'gpt-4.1',
85
+ 'gpt-4.1-mini',
86
+ 'gpt-4.1-nano',
87
+ 'gpt-4-turbo',
88
+ 'gpt-4',
89
+ 'gpt-3.5-turbo',
90
+ 'o3',
91
+ 'o4-mini',
92
+ 'chatgpt-4o-latest'
93
+ ]
94
+ },
95
+ gemini: {
96
+ provider: 'google',
97
+ description: 'Google Gemini models',
98
+ models: [
99
+ 'gemini-1.5-pro',
100
+ 'gemini-1.5-flash',
101
+ 'gemini-2.0-flash',
102
+ 'gemini-2.0-flash-exp',
103
+ 'gemini-2.0-flash-thinking',
104
+ 'gemini-2.0-flash-thinking-exp',
105
+ 'gemini-2.0-pro',
106
+ 'gemini-2.5-flash',
107
+ 'gemini-2.5-flash-lite',
108
+ 'gemini-2.5-pro'
109
+ ]
110
+ }
111
+ }
112
+ }
113
+
114
+ /**
115
+ * 保存默认配置到文件
116
+ */
117
+ saveDefaultConfig() {
118
+ try {
119
+ const dataDir = path.dirname(this.modelsFile)
120
+ if (!fs.existsSync(dataDir)) {
121
+ fs.mkdirSync(dataDir, { recursive: true })
122
+ }
123
+
124
+ fs.writeFileSync(this.modelsFile, JSON.stringify(this.supportedModels, null, 2))
125
+ logger.info('💾 Created default supported_models.json configuration')
126
+ } catch (error) {
127
+ logger.error('❌ Failed to save default config:', error)
128
+ }
129
+ }
130
+
131
+ /**
132
+ * 获取所有支持的模型(OpenAI API 格式)
133
+ */
134
+ getAllModels() {
135
+ const models = []
136
+ const now = Math.floor(Date.now() / 1000)
137
+
138
+ for (const [_service, config] of Object.entries(this.supportedModels)) {
139
+ for (const modelId of config.models) {
140
+ models.push({
141
+ id: modelId,
142
+ object: 'model',
143
+ created: now,
144
+ owned_by: config.provider
145
+ })
146
+ }
147
+ }
148
+
149
+ return models.sort((a, b) => {
150
+ // 先按 provider 排序,再按 model id 排序
151
+ if (a.owned_by !== b.owned_by) {
152
+ return a.owned_by.localeCompare(b.owned_by)
153
+ }
154
+ return a.id.localeCompare(b.id)
155
+ })
156
+ }
157
+
158
+ /**
159
+ * 按 provider 获取模型
160
+ * @param {string} provider - 'anthropic', 'openai', 'google' 等
161
+ */
162
+ getModelsByProvider(provider) {
163
+ return this.getAllModels().filter((m) => m.owned_by === provider)
164
+ }
165
+
166
+ /**
167
+ * 检查模型是否被支持
168
+ * @param {string} modelId - 模型 ID
169
+ */
170
+ isModelSupported(modelId) {
171
+ if (!modelId) {
172
+ return false
173
+ }
174
+ return this.getAllModels().some((m) => m.id === modelId)
175
+ }
176
+
177
+ /**
178
+ * 获取模型的 provider
179
+ * @param {string} modelId - 模型 ID
180
+ */
181
+ getModelProvider(modelId) {
182
+ const model = this.getAllModels().find((m) => m.id === modelId)
183
+ return model ? model.owned_by : null
184
+ }
185
+
186
+ /**
187
+ * 重新加载模型配置
188
+ */
189
+ reloadModels() {
190
+ logger.info('🔄 Reloading supported models configuration...')
191
+ this.loadModels()
192
+ }
193
+
194
+ /**
195
+ * 设置文件监听器(监听配置文件变化)
196
+ */
197
+ setupFileWatcher() {
198
+ try {
199
+ // 如果已有监听器,先关闭
200
+ if (this.fileWatcher) {
201
+ this.fileWatcher.close()
202
+ this.fileWatcher = null
203
+ }
204
+
205
+ // 只有文件存在时才设置监听器
206
+ if (!fs.existsSync(this.modelsFile)) {
207
+ logger.debug('📋 Models file does not exist yet, skipping file watcher setup')
208
+ return
209
+ }
210
+
211
+ // 使用 fs.watchFile 监听文件变化
212
+ const watchOptions = {
213
+ persistent: true,
214
+ interval: 60000 // 每60秒检查一次
215
+ }
216
+
217
+ let lastMtime = fs.statSync(this.modelsFile).mtimeMs
218
+
219
+ fs.watchFile(this.modelsFile, watchOptions, (curr, _prev) => {
220
+ if (curr.mtimeMs !== lastMtime) {
221
+ lastMtime = curr.mtimeMs
222
+ logger.info('📋 Detected change in supported_models.json, reloading...')
223
+ this.reloadModels()
224
+ }
225
+ })
226
+
227
+ // 保存引用以便清理
228
+ this.fileWatcher = {
229
+ close: () => fs.unwatchFile(this.modelsFile)
230
+ }
231
+
232
+ logger.info('👁️ File watcher set up for supported_models.json')
233
+ } catch (error) {
234
+ logger.error('❌ Failed to setup file watcher:', error)
235
+ }
236
+ }
237
+
238
+ /**
239
+ * 获取服务状态
240
+ */
241
+ getStatus() {
242
+ const totalModels = this.supportedModels
243
+ ? Object.values(this.supportedModels).reduce((sum, config) => sum + config.models.length, 0)
244
+ : 0
245
+
246
+ return {
247
+ initialized: this.supportedModels !== null,
248
+ totalModels,
249
+ providers: this.supportedModels ? Object.keys(this.supportedModels) : [],
250
+ fileExists: fs.existsSync(this.modelsFile)
251
+ }
252
+ }
253
+
254
+ /**
255
+ * 清理资源
256
+ */
257
+ cleanup() {
258
+ if (this.fileWatcher) {
259
+ this.fileWatcher.close()
260
+ this.fileWatcher = null
261
+ logger.debug('📋 Model service file watcher closed')
262
+ }
263
+ }
264
+ }
265
+
266
+ module.exports = new ModelService()
src/services/openaiAccountService.js CHANGED
@@ -194,19 +194,6 @@ function buildCodexUsageSnapshot(accountData) {
194
  }
195
  }
196
 
197
- function normalizeSubscriptionExpiresAt(value) {
198
- if (value === undefined || value === null || value === '') {
199
- return ''
200
- }
201
-
202
- const date = value instanceof Date ? value : new Date(value)
203
- if (Number.isNaN(date.getTime())) {
204
- return ''
205
- }
206
-
207
- return date.toISOString()
208
- }
209
-
210
  // 刷新访问令牌
211
  async function refreshAccessToken(refreshToken, proxy = null) {
212
  try {
@@ -347,6 +334,19 @@ function isTokenExpired(account) {
347
  return new Date(account.expiresAt) <= new Date()
348
  }
349
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  // 刷新账户的 access token(带分布式锁)
351
  async function refreshAccountToken(accountId) {
352
  let lockAcquired = false
@@ -530,13 +530,6 @@ async function createAccount(accountData) {
530
  // 处理账户信息
531
  const accountInfo = accountData.accountInfo || {}
532
 
533
- const tokenExpiresAt = oauthData.expires_in
534
- ? new Date(Date.now() + oauthData.expires_in * 1000).toISOString()
535
- : ''
536
- const subscriptionExpiresAt = normalizeSubscriptionExpiresAt(
537
- accountData.subscriptionExpiresAt || accountInfo.subscriptionExpiresAt || ''
538
- )
539
-
540
  // 检查邮箱是否已经是加密格式(包含冒号分隔的32位十六进制字符)
541
  const isEmailEncrypted =
542
  accountInfo.email && accountInfo.email.length >= 33 && accountInfo.email.charAt(32) === ':'
@@ -573,8 +566,13 @@ async function createAccount(accountData) {
573
  email: isEmailEncrypted ? accountInfo.email : encrypt(accountInfo.email || ''),
574
  emailVerified: accountInfo.emailVerified === true ? 'true' : 'false',
575
  // 过期时间
576
- expiresAt: tokenExpiresAt,
577
- subscriptionExpiresAt,
 
 
 
 
 
578
  // 状态字段
579
  isActive: accountData.isActive !== false ? 'true' : 'false',
580
  status: 'active',
@@ -599,10 +597,7 @@ async function createAccount(accountData) {
599
  }
600
 
601
  logger.info(`Created OpenAI account: ${accountId}`)
602
- return {
603
- ...account,
604
- subscriptionExpiresAt: account.subscriptionExpiresAt || null
605
- }
606
  }
607
 
608
  // 获取账户
@@ -645,11 +640,6 @@ async function getAccount(accountId) {
645
  }
646
  }
647
 
648
- accountData.subscriptionExpiresAt =
649
- accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== ''
650
- ? accountData.subscriptionExpiresAt
651
- : null
652
-
653
  return accountData
654
  }
655
 
@@ -683,16 +673,18 @@ async function updateAccount(accountId, updates) {
683
  updates.email = encrypt(updates.email)
684
  }
685
 
686
- if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
687
- updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.subscriptionExpiresAt)
688
- }
689
-
690
  // 处理代理配置
691
  if (updates.proxy) {
692
  updates.proxy =
693
  typeof updates.proxy === 'string' ? updates.proxy : JSON.stringify(updates.proxy)
694
  }
695
 
 
 
 
 
 
 
696
  // 更新账户类型时处理共享账户集合
697
  const client = redisClient.getClientSafe()
698
  if (updates.accountType && updates.accountType !== existingAccount.accountType) {
@@ -719,10 +711,6 @@ async function updateAccount(accountId, updates) {
719
  }
720
  }
721
 
722
- if (!updatedAccount.subscriptionExpiresAt) {
723
- updatedAccount.subscriptionExpiresAt = null
724
- }
725
-
726
  return updatedAccount
727
  }
728
 
@@ -805,7 +793,11 @@ async function getAllAccounts() {
805
  }
806
  }
807
 
808
- const subscriptionExpiresAt = accountData.subscriptionExpiresAt || null
 
 
 
 
809
 
810
  // 不解密敏感字段,只返回基本信息
811
  accounts.push({
@@ -815,13 +807,18 @@ async function getAllAccounts() {
815
  openaiOauth: maskedOauth,
816
  accessToken: maskedAccessToken,
817
  refreshToken: maskedRefreshToken,
 
 
 
 
 
 
818
  // 添加 scopes 字段用于判断认证方式
819
  // 处理空字符串的情况
820
  scopes:
821
  accountData.scopes && accountData.scopes.trim() ? accountData.scopes.split(' ') : [],
822
  // 添加 hasRefreshToken 标记
823
  hasRefreshToken: hasRefreshTokenFlag,
824
- subscriptionExpiresAt,
825
  // 添加限流状态信息(统一格式)
826
  rateLimitStatus: rateLimitInfo
827
  ? {
@@ -940,8 +937,17 @@ async function selectAvailableAccount(apiKeyId, sessionHash = null) {
940
 
941
  for (const accountId of sharedAccountIds) {
942
  const account = await getAccount(accountId)
943
- if (account && account.isActive === 'true' && !isRateLimited(account)) {
 
 
 
 
 
944
  availableAccounts.push(account)
 
 
 
 
945
  }
946
  }
947
 
 
194
  }
195
  }
196
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  // 刷新访问令牌
198
  async function refreshAccessToken(refreshToken, proxy = null) {
199
  try {
 
334
  return new Date(account.expiresAt) <= new Date()
335
  }
336
 
337
+ /**
338
+ * 检查账户订阅是否过期
339
+ * @param {Object} account - 账户对象
340
+ * @returns {boolean} - true: 已过期, false: 未过期
341
+ */
342
+ function isSubscriptionExpired(account) {
343
+ if (!account.subscriptionExpiresAt) {
344
+ return false // 未设置视为永不过期
345
+ }
346
+ const expiryDate = new Date(account.subscriptionExpiresAt)
347
+ return expiryDate <= new Date()
348
+ }
349
+
350
  // 刷新账户的 access token(带分布式锁)
351
  async function refreshAccountToken(accountId) {
352
  let lockAcquired = false
 
530
  // 处理账户信息
531
  const accountInfo = accountData.accountInfo || {}
532
 
 
 
 
 
 
 
 
533
  // 检查邮箱是否已经是加密格式(包含冒号分隔的32位十六进制字符)
534
  const isEmailEncrypted =
535
  accountInfo.email && accountInfo.email.length >= 33 && accountInfo.email.charAt(32) === ':'
 
566
  email: isEmailEncrypted ? accountInfo.email : encrypt(accountInfo.email || ''),
567
  emailVerified: accountInfo.emailVerified === true ? 'true' : 'false',
568
  // 过期时间
569
+ expiresAt: oauthData.expires_in
570
+ ? new Date(Date.now() + oauthData.expires_in * 1000).toISOString()
571
+ : new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(), // OAuth Token 过期时间(技术字段)
572
+
573
+ // ✅ 新增:账户订阅到期时间(业务字段,手动管理)
574
+ subscriptionExpiresAt: accountData.subscriptionExpiresAt || null,
575
+
576
  // 状态字段
577
  isActive: accountData.isActive !== false ? 'true' : 'false',
578
  status: 'active',
 
597
  }
598
 
599
  logger.info(`Created OpenAI account: ${accountId}`)
600
+ return account
 
 
 
601
  }
602
 
603
  // 获取账户
 
640
  }
641
  }
642
 
 
 
 
 
 
643
  return accountData
644
  }
645
 
 
673
  updates.email = encrypt(updates.email)
674
  }
675
 
 
 
 
 
676
  // 处理代理配置
677
  if (updates.proxy) {
678
  updates.proxy =
679
  typeof updates.proxy === 'string' ? updates.proxy : JSON.stringify(updates.proxy)
680
  }
681
 
682
+ // ✅ 如果通过路由映射更新了 subscriptionExpiresAt,直接保存
683
+ // subscriptionExpiresAt 是业务字段,与 token 刷新独立
684
+ if (updates.subscriptionExpiresAt !== undefined) {
685
+ // 直接保存,不做任何调整
686
+ }
687
+
688
  // 更新账户类型时处理共享账户集合
689
  const client = redisClient.getClientSafe()
690
  if (updates.accountType && updates.accountType !== existingAccount.accountType) {
 
711
  }
712
  }
713
 
 
 
 
 
714
  return updatedAccount
715
  }
716
 
 
793
  }
794
  }
795
 
796
+ const tokenExpiresAt = accountData.expiresAt || null
797
+ const subscriptionExpiresAt =
798
+ accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== ''
799
+ ? accountData.subscriptionExpiresAt
800
+ : null
801
 
802
  // 不解密敏感字段,只返回基本信息
803
  accounts.push({
 
807
  openaiOauth: maskedOauth,
808
  accessToken: maskedAccessToken,
809
  refreshToken: maskedRefreshToken,
810
+
811
+ // ✅ 前端显示订阅过期时间(业务字段)
812
+ tokenExpiresAt,
813
+ subscriptionExpiresAt,
814
+ expiresAt: subscriptionExpiresAt,
815
+
816
  // 添加 scopes 字段用于判断认证方式
817
  // 处理空字符串的情况
818
  scopes:
819
  accountData.scopes && accountData.scopes.trim() ? accountData.scopes.split(' ') : [],
820
  // 添加 hasRefreshToken 标记
821
  hasRefreshToken: hasRefreshTokenFlag,
 
822
  // 添加限流状态信息(统一格式)
823
  rateLimitStatus: rateLimitInfo
824
  ? {
 
937
 
938
  for (const accountId of sharedAccountIds) {
939
  const account = await getAccount(accountId)
940
+ if (
941
+ account &&
942
+ account.isActive === 'true' &&
943
+ !isRateLimited(account) &&
944
+ !isSubscriptionExpired(account)
945
+ ) {
946
  availableAccounts.push(account)
947
+ } else if (account && isSubscriptionExpired(account)) {
948
+ logger.debug(
949
+ `⏰ Skipping expired OpenAI account: ${account.name}, expired at ${account.subscriptionExpiresAt}`
950
+ )
951
  }
952
  }
953
 
src/services/openaiResponsesAccountService.js CHANGED
@@ -5,19 +5,6 @@ const logger = require('../utils/logger')
5
  const config = require('../../config/config')
6
  const LRUCache = require('../utils/lruCache')
7
 
8
- function normalizeSubscriptionExpiresAt(value) {
9
- if (value === undefined || value === null || value === '') {
10
- return ''
11
- }
12
-
13
- const date = value instanceof Date ? value : new Date(value)
14
- if (Number.isNaN(date.getTime())) {
15
- return ''
16
- }
17
-
18
- return date.toISOString()
19
- }
20
-
21
  class OpenAIResponsesAccountService {
22
  constructor() {
23
  // 加密相关常量
@@ -62,8 +49,7 @@ class OpenAIResponsesAccountService {
62
  schedulable = true, // 是否可被调度
63
  dailyQuota = 0, // 每日额度限制(美元),0表示不限制
64
  quotaResetTime = '00:00', // 额度重置时间(HH:mm格式)
65
- rateLimitDuration = 60, // 限流时间(分钟)
66
- subscriptionExpiresAt = null
67
  } = options
68
 
69
  // 验证必填字段
@@ -89,6 +75,11 @@ class OpenAIResponsesAccountService {
89
  isActive: isActive.toString(),
90
  accountType,
91
  schedulable: schedulable.toString(),
 
 
 
 
 
92
  createdAt: new Date().toISOString(),
93
  lastUsedAt: '',
94
  status: 'active',
@@ -102,8 +93,7 @@ class OpenAIResponsesAccountService {
102
  dailyUsage: '0',
103
  lastResetDate: redis.getDateStringInTimezone(),
104
  quotaResetTime,
105
- quotaStoppedAt: '',
106
- subscriptionExpiresAt: normalizeSubscriptionExpiresAt(subscriptionExpiresAt)
107
  }
108
 
109
  // 保存到 Redis
@@ -113,7 +103,6 @@ class OpenAIResponsesAccountService {
113
 
114
  return {
115
  ...accountData,
116
- subscriptionExpiresAt: accountData.subscriptionExpiresAt || null,
117
  apiKey: '***' // 返回时隐藏敏感信息
118
  }
119
  }
@@ -140,11 +129,6 @@ class OpenAIResponsesAccountService {
140
  }
141
  }
142
 
143
- accountData.subscriptionExpiresAt =
144
- accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== ''
145
- ? accountData.subscriptionExpiresAt
146
- : null
147
-
148
  return accountData
149
  }
150
 
@@ -172,11 +156,10 @@ class OpenAIResponsesAccountService {
172
  : updates.baseApi
173
  }
174
 
175
- if (Object.prototype.hasOwnProperty.call(updates, 'subscriptionExpiresAt')) {
176
- updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.subscriptionExpiresAt)
177
- } else if (Object.prototype.hasOwnProperty.call(updates, 'expiresAt')) {
178
- updates.subscriptionExpiresAt = normalizeSubscriptionExpiresAt(updates.expiresAt)
179
- delete updates.expiresAt
180
  }
181
 
182
  // 更新 Redis
@@ -240,6 +223,10 @@ class OpenAIResponsesAccountService {
240
  // 转换 isActive 字段为布尔值
241
  account.isActive = account.isActive === 'true'
242
 
 
 
 
 
243
  accounts.push(account)
244
  }
245
  }
@@ -285,10 +272,10 @@ class OpenAIResponsesAccountService {
285
  accountData.schedulable = accountData.schedulable !== 'false'
286
  // 转换 isActive 字段为布尔值
287
  accountData.isActive = accountData.isActive === 'true'
288
- accountData.subscriptionExpiresAt =
289
- accountData.subscriptionExpiresAt && accountData.subscriptionExpiresAt !== ''
290
- ? accountData.subscriptionExpiresAt
291
- : null
292
 
293
  accounts.push(accountData)
294
  }
@@ -536,6 +523,25 @@ class OpenAIResponsesAccountService {
536
  return { success: true, message: 'Account status reset successfully' }
537
  }
538
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  // 获取限流信息
540
  _getRateLimitInfo(accountData) {
541
  if (accountData.rateLimitStatus !== 'limited') {
 
5
  const config = require('../../config/config')
6
  const LRUCache = require('../utils/lruCache')
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  class OpenAIResponsesAccountService {
9
  constructor() {
10
  // 加密相关常量
 
49
  schedulable = true, // 是否可被调度
50
  dailyQuota = 0, // 每日额度限制(美元),0表示不限制
51
  quotaResetTime = '00:00', // 额度重置时间(HH:mm格式)
52
+ rateLimitDuration = 60 // 限流时间(分钟)
 
53
  } = options
54
 
55
  // 验证必填字段
 
75
  isActive: isActive.toString(),
76
  accountType,
77
  schedulable: schedulable.toString(),
78
+
79
+ // ✅ 新增:账户订阅到期时间(业务字段,手动管理)
80
+ // 注意:OpenAI-Responses 使用 API Key 认证,没有 OAuth token,因此没有 expiresAt
81
+ subscriptionExpiresAt: options.subscriptionExpiresAt || null,
82
+
83
  createdAt: new Date().toISOString(),
84
  lastUsedAt: '',
85
  status: 'active',
 
93
  dailyUsage: '0',
94
  lastResetDate: redis.getDateStringInTimezone(),
95
  quotaResetTime,
96
+ quotaStoppedAt: ''
 
97
  }
98
 
99
  // 保存到 Redis
 
103
 
104
  return {
105
  ...accountData,
 
106
  apiKey: '***' // 返回时隐藏敏感信息
107
  }
108
  }
 
129
  }
130
  }
131
 
 
 
 
 
 
132
  return accountData
133
  }
134
 
 
156
  : updates.baseApi
157
  }
158
 
159
+ // 直接保存 subscriptionExpiresAt(如果提供)
160
+ // OpenAI-Responses 使用 API Key,没有 token 刷新逻辑,不会覆盖此字段
161
+ if (updates.subscriptionExpiresAt !== undefined) {
162
+ // 直接保存,不做任何调整
 
163
  }
164
 
165
  // 更新 Redis
 
223
  // 转换 isActive 字段为布尔值
224
  account.isActive = account.isActive === 'true'
225
 
226
+ // ✅ 前端显示订阅过期时间(业务字段)
227
+ account.expiresAt = account.subscriptionExpiresAt || null
228
+ account.platform = account.platform || 'openai-responses'
229
+
230
  accounts.push(account)
231
  }
232
  }
 
272
  accountData.schedulable = accountData.schedulable !== 'false'
273
  // 转换 isActive 字段为布尔值
274
  accountData.isActive = accountData.isActive === 'true'
275
+
276
+ // 前端显示订阅过期时间(业务字段)
277
+ accountData.expiresAt = accountData.subscriptionExpiresAt || null
278
+ accountData.platform = accountData.platform || 'openai-responses'
279
 
280
  accounts.push(accountData)
281
  }
 
523
  return { success: true, message: 'Account status reset successfully' }
524
  }
525
 
526
+ // ⏰ 检查账户订阅是否已过期
527
+ isSubscriptionExpired(account) {
528
+ if (!account.subscriptionExpiresAt) {
529
+ return false // 未设置过期时间,视为永不过期
530
+ }
531
+
532
+ const expiryDate = new Date(account.subscriptionExpiresAt)
533
+ const now = new Date()
534
+
535
+ if (expiryDate <= now) {
536
+ logger.debug(
537
+ `⏰ OpenAI-Responses Account ${account.name} (${account.id}) subscription expired at ${account.subscriptionExpiresAt}`
538
+ )
539
+ return true
540
+ }
541
+
542
+ return false
543
+ }
544
+
545
  // 获取限流信息
546
  _getRateLimitInfo(accountData) {
547
  if (accountData.rateLimitStatus !== 'limited') {
src/services/openaiToClaude.js CHANGED
@@ -31,10 +31,25 @@ class OpenAIToClaudeConverter {
31
  stream: openaiRequest.stream || false
32
  }
33
 
34
- // Claude Code 必需的系统消息
35
  const claudeCodeSystemMessage = "You are Claude Code, Anthropic's official CLI for Claude."
36
 
37
- claudeRequest.system = claudeCodeSystemMessage
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
  // 处理停止序列
40
  if (openaiRequest.stop) {
 
31
  stream: openaiRequest.stream || false
32
  }
33
 
34
+ // 定义 Claude Code 的默认系统提示词
35
  const claudeCodeSystemMessage = "You are Claude Code, Anthropic's official CLI for Claude."
36
 
37
+ // 如果 OpenAI 请求中包含系统消息,提取并检查
38
+ const systemMessage = this._extractSystemMessage(openaiRequest.messages)
39
+ if (systemMessage && systemMessage.includes('You are currently in Xcode')) {
40
+ // Xcode 系统提示词
41
+ claudeRequest.system = systemMessage
42
+ logger.info(
43
+ `🔍 Xcode request detected, using Xcode system prompt (${systemMessage.length} chars)`
44
+ )
45
+ logger.debug(`📋 System prompt preview: ${systemMessage.substring(0, 150)}...`)
46
+ } else {
47
+ // 使用 Claude Code 默认系统提示词
48
+ claudeRequest.system = claudeCodeSystemMessage
49
+ logger.debug(
50
+ `📋 Using Claude Code default system prompt${systemMessage ? ' (ignored custom prompt)' : ''}`
51
+ )
52
+ }
53
 
54
  // 处理停止序列
55
  if (openaiRequest.stop) {
src/services/unifiedClaudeScheduler.js CHANGED
@@ -545,6 +545,14 @@ class UnifiedClaudeScheduler {
545
  continue
546
  }
547
 
 
 
 
 
 
 
 
 
548
  // 主动触发一次额度检查,确保状态即时生效
549
  try {
550
  await claudeConsoleAccountService.checkQuotaUsage(account.id)
@@ -642,6 +650,14 @@ class UnifiedClaudeScheduler {
642
  continue
643
  }
644
 
 
 
 
 
 
 
 
 
645
  // 检查是否被限流
646
  const isRateLimited = await ccrAccountService.isAccountRateLimited(account.id)
647
  const isQuotaExceeded = await ccrAccountService.isAccountQuotaExceeded(account.id)
@@ -774,6 +790,13 @@ class UnifiedClaudeScheduler {
774
  ) {
775
  return false
776
  }
 
 
 
 
 
 
 
777
  // 检查是否超额
778
  try {
779
  await claudeConsoleAccountService.checkQuotaUsage(accountId)
@@ -832,6 +855,13 @@ class UnifiedClaudeScheduler {
832
  if (!this._isModelSupportedByAccount(account, 'ccr', requestedModel, 'in session check')) {
833
  return false
834
  }
 
 
 
 
 
 
 
835
  // 检查是否超额
836
  try {
837
  await ccrAccountService.checkQuotaUsage(accountId)
@@ -1353,6 +1383,14 @@ class UnifiedClaudeScheduler {
1353
  continue
1354
  }
1355
 
 
 
 
 
 
 
 
 
1356
  // 检查是否被限流或超额
1357
  const isRateLimited = await ccrAccountService.isAccountRateLimited(account.id)
1358
  const isQuotaExceeded = await ccrAccountService.isAccountQuotaExceeded(account.id)
 
545
  continue
546
  }
547
 
548
+ // 检查订阅是否过期
549
+ if (claudeConsoleAccountService.isSubscriptionExpired(account)) {
550
+ logger.debug(
551
+ `⏰ Claude Console account ${account.name} (${account.id}) expired at ${account.subscriptionExpiresAt}`
552
+ )
553
+ continue
554
+ }
555
+
556
  // 主动触发一次额度检查,确保状态即时生效
557
  try {
558
  await claudeConsoleAccountService.checkQuotaUsage(account.id)
 
650
  continue
651
  }
652
 
653
+ // 检查订阅是否过期
654
+ if (ccrAccountService.isSubscriptionExpired(account)) {
655
+ logger.debug(
656
+ `⏰ CCR account ${account.name} (${account.id}) expired at ${account.subscriptionExpiresAt}`
657
+ )
658
+ continue
659
+ }
660
+
661
  // 检查是否被限流
662
  const isRateLimited = await ccrAccountService.isAccountRateLimited(account.id)
663
  const isQuotaExceeded = await ccrAccountService.isAccountQuotaExceeded(account.id)
 
790
  ) {
791
  return false
792
  }
793
+ // 检查订阅是否过期
794
+ if (claudeConsoleAccountService.isSubscriptionExpired(account)) {
795
+ logger.debug(
796
+ `⏰ Claude Console account ${account.name} (${accountId}) expired at ${account.subscriptionExpiresAt} (session check)`
797
+ )
798
+ return false
799
+ }
800
  // 检查是否超额
801
  try {
802
  await claudeConsoleAccountService.checkQuotaUsage(accountId)
 
855
  if (!this._isModelSupportedByAccount(account, 'ccr', requestedModel, 'in session check')) {
856
  return false
857
  }
858
+ // 检查订阅是否过期
859
+ if (ccrAccountService.isSubscriptionExpired(account)) {
860
+ logger.debug(
861
+ `⏰ CCR account ${account.name} (${accountId}) expired at ${account.subscriptionExpiresAt} (session check)`
862
+ )
863
+ return false
864
+ }
865
  // 检查是否超额
866
  try {
867
  await ccrAccountService.checkQuotaUsage(accountId)
 
1383
  continue
1384
  }
1385
 
1386
+ // 检查订阅是否过期
1387
+ if (ccrAccountService.isSubscriptionExpired(account)) {
1388
+ logger.debug(
1389
+ `⏰ CCR account ${account.name} (${account.id}) expired at ${account.subscriptionExpiresAt}`
1390
+ )
1391
+ continue
1392
+ }
1393
+
1394
  // 检查是否被限流或超额
1395
  const isRateLimited = await ccrAccountService.isAccountRateLimited(account.id)
1396
  const isQuotaExceeded = await ccrAccountService.isAccountQuotaExceeded(account.id)
src/services/unifiedOpenAIScheduler.js CHANGED
@@ -211,6 +211,15 @@ class UnifiedOpenAIScheduler {
211
  error.statusCode = 403 // Forbidden - 调度被禁止
212
  throw error
213
  }
 
 
 
 
 
 
 
 
 
214
  }
215
 
216
  // 专属账户:可选的模型检查(只有明确配置了supportedModels且不为空才检查)
@@ -461,6 +470,14 @@ class UnifiedOpenAIScheduler {
461
  }
462
  }
463
 
 
 
 
 
 
 
 
 
464
  // OpenAI-Responses 账户默认支持所有模型
465
  // 因为它们是第三方兼容 API,模型支持由第三方决定
466
 
@@ -536,6 +553,11 @@ class UnifiedOpenAIScheduler {
536
  logger.info(`🚫 OpenAI-Responses account ${accountId} is not schedulable`)
537
  return false
538
  }
 
 
 
 
 
539
  // 检查并清除过期的限流状态
540
  const isRateLimitCleared =
541
  await openaiResponsesAccountService.checkAndClearRateLimit(accountId)
 
211
  error.statusCode = 403 // Forbidden - 调度被禁止
212
  throw error
213
  }
214
+
215
+ // ⏰ 检查 OpenAI-Responses 专属账户订阅是否过期
216
+ if (openaiResponsesAccountService.isSubscriptionExpired(boundAccount)) {
217
+ const errorMsg = `Dedicated account ${boundAccount.name} subscription has expired`
218
+ logger.warn(`⚠️ ${errorMsg}`)
219
+ const error = new Error(errorMsg)
220
+ error.statusCode = 403 // Forbidden - 订阅已过期
221
+ throw error
222
+ }
223
  }
224
 
225
  // 专属账户:可选的模型检查(只有明确配置了supportedModels且不为空才检查)
 
470
  }
471
  }
472
 
473
+ // ⏰ 检查订阅是否过期
474
+ if (openaiResponsesAccountService.isSubscriptionExpired(account)) {
475
+ logger.debug(
476
+ `⏭️ Skipping OpenAI-Responses account ${account.name} - subscription expired`
477
+ )
478
+ continue
479
+ }
480
+
481
  // OpenAI-Responses 账户默认支持所有模型
482
  // 因为它们是第三方兼容 API,模型支持由第三方决定
483
 
 
553
  logger.info(`🚫 OpenAI-Responses account ${accountId} is not schedulable`)
554
  return false
555
  }
556
+ // ⏰ 检查订阅是否过期
557
+ if (openaiResponsesAccountService.isSubscriptionExpired(account)) {
558
+ logger.info(`🚫 OpenAI-Responses account ${accountId} subscription expired`)
559
+ return false
560
+ }
561
  // 检查并清除过期的限流状态
562
  const isRateLimitCleared =
563
  await openaiResponsesAccountService.checkAndClearRateLimit(accountId)
src/utils/runtimeAddon.js ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const logger = require('./logger')
4
+
5
+ const ADDON_DIRECTORIES = [
6
+ path.join(process.cwd(), '.local', 'ext'),
7
+ path.join(process.cwd(), '.local', 'extensions')
8
+ ]
9
+
10
+ class RuntimeAddonBus {
11
+ constructor() {
12
+ this._handlers = new Map()
13
+ this._initialized = false
14
+ }
15
+
16
+ register(eventId, handler) {
17
+ if (!eventId || typeof handler !== 'function') {
18
+ return
19
+ }
20
+
21
+ if (!this._handlers.has(eventId)) {
22
+ this._handlers.set(eventId, [])
23
+ }
24
+
25
+ this._handlers.get(eventId).push(handler)
26
+ }
27
+
28
+ emitSync(eventId, payload) {
29
+ this._ensureInitialized()
30
+
31
+ if (!eventId) {
32
+ return payload
33
+ }
34
+
35
+ const handlers = this._handlers.get(eventId)
36
+ if (!handlers || handlers.length === 0) {
37
+ return payload
38
+ }
39
+
40
+ let current = payload
41
+
42
+ for (const handler of handlers) {
43
+ try {
44
+ const result = handler(current)
45
+ if (typeof result !== 'undefined') {
46
+ current = result
47
+ }
48
+ } catch (error) {
49
+ this._log('warn', `本地扩展处理 ${eventId} 失败: ${error.message}`, error)
50
+ }
51
+ }
52
+
53
+ return current
54
+ }
55
+
56
+ _ensureInitialized() {
57
+ if (this._initialized) {
58
+ return
59
+ }
60
+
61
+ this._initialized = true
62
+ const loadedPaths = new Set()
63
+
64
+ for (const dir of ADDON_DIRECTORIES) {
65
+ if (!dir || !fs.existsSync(dir)) {
66
+ continue
67
+ }
68
+
69
+ let entries = []
70
+ try {
71
+ entries = fs.readdirSync(dir, { withFileTypes: true })
72
+ } catch (error) {
73
+ this._log('warn', `读取本地扩展目录 ${dir} 失败: ${error.message}`, error)
74
+ continue
75
+ }
76
+
77
+ for (const entry of entries) {
78
+ if (!entry.isFile()) {
79
+ continue
80
+ }
81
+
82
+ if (!entry.name.endsWith('.js')) {
83
+ continue
84
+ }
85
+
86
+ const targetPath = path.join(dir, entry.name)
87
+
88
+ if (loadedPaths.has(targetPath)) {
89
+ continue
90
+ }
91
+
92
+ loadedPaths.add(targetPath)
93
+
94
+ try {
95
+ const registrar = require(targetPath)
96
+ if (typeof registrar === 'function') {
97
+ registrar(this)
98
+ }
99
+ } catch (error) {
100
+ this._log('warn', `加载本地扩展 ${entry.name} 失败: ${error.message}`, error)
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ _log(level, message, error) {
107
+ const targetLevel = typeof level === 'string' ? level : 'info'
108
+ const loggerMethod =
109
+ logger && typeof logger[targetLevel] === 'function' ? logger[targetLevel].bind(logger) : null
110
+
111
+ if (loggerMethod) {
112
+ loggerMethod(message, error)
113
+ } else if (targetLevel === 'error') {
114
+ console.error(message, error)
115
+ } else {
116
+ console.log(message, error)
117
+ }
118
+ }
119
+ }
120
+
121
+ module.exports = new RuntimeAddonBus()
web/admin-spa/src/components/accounts/AccountExpiryEditModal.vue CHANGED
@@ -304,7 +304,25 @@ const selectQuickOption = (value) => {
304
  // 更新自定义过期时间
305
  const updateCustomExpiryPreview = () => {
306
  if (localForm.customExpireDate) {
307
- localForm.expiresAt = new Date(localForm.customExpireDate).toISOString()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  }
309
  }
310
 
 
304
  // 更新自定义过期时间
305
  const updateCustomExpiryPreview = () => {
306
  if (localForm.customExpireDate) {
307
+ try {
308
+ // 手动解析日期时间字符串,确保它被正确解释为本地时间
309
+ const [datePart, timePart] = localForm.customExpireDate.split('T')
310
+ const [year, month, day] = datePart.split('-').map(Number)
311
+ const [hours, minutes] = timePart.split(':').map(Number)
312
+
313
+ // 使用构造函数创建本地时间的 Date 对象,然后转换为 UTC ISO 字符串
314
+ const localDate = new Date(year, month - 1, day, hours, minutes, 0, 0)
315
+
316
+ // 验证日期有效性
317
+ if (isNaN(localDate.getTime())) {
318
+ console.error('Invalid date:', localForm.customExpireDate)
319
+ return
320
+ }
321
+
322
+ localForm.expiresAt = localDate.toISOString()
323
+ } catch (error) {
324
+ console.error('Failed to parse custom expire date:', error)
325
+ }
326
  }
327
  }
328
 
web/admin-spa/src/components/accounts/AccountForm.vue CHANGED
@@ -4841,11 +4841,23 @@ const handleGroupRefresh = async () => {
4841
  // 处理 API Key 管理模态框刷新
4842
  const handleApiKeyRefresh = async () => {
4843
  // 刷新账户信息以更新 API Key 数量
4844
- if (props.account?.id) {
 
 
 
 
 
 
 
 
 
 
 
4845
  try {
4846
- await accountsStore.fetchAccounts()
 
4847
  } catch (error) {
4848
- console.error('Failed to refresh account data:', error)
4849
  }
4850
  }
4851
  }
 
4841
  // 处理 API Key 管理模态框刷新
4842
  const handleApiKeyRefresh = async () => {
4843
  // 刷新账户信息以更新 API Key 数量
4844
+ if (!props.account?.id) {
4845
+ return
4846
+ }
4847
+
4848
+ const refreshers = [
4849
+ typeof accountsStore.fetchDroidAccounts === 'function'
4850
+ ? accountsStore.fetchDroidAccounts
4851
+ : null,
4852
+ typeof accountsStore.fetchAllAccounts === 'function' ? accountsStore.fetchAllAccounts : null
4853
+ ].filter(Boolean)
4854
+
4855
+ for (const refresher of refreshers) {
4856
  try {
4857
+ await refresher()
4858
+ return
4859
  } catch (error) {
4860
+ console.error('刷新账户列表失败:', error)
4861
  }
4862
  }
4863
  }
web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue CHANGED
@@ -20,12 +20,28 @@
20
  </p>
21
  </div>
22
  </div>
23
- <button
24
- class="p-1 text-gray-400 transition-colors hover:text-gray-600 dark:hover:text-gray-300"
25
- @click="$emit('close')"
26
- >
27
- <i class="fas fa-times text-lg sm:text-xl" />
28
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  </div>
30
 
31
  <!-- 加载状态 -->
@@ -239,6 +255,7 @@ const resetting = ref(null)
239
  const apiKeys = ref([])
240
  const currentPage = ref(1)
241
  const pageSize = ref(18)
 
242
 
243
  // 计算属性
244
  const totalItems = computed(() => apiKeys.value.length)
@@ -403,14 +420,71 @@ const maskApiKey = (key) => {
403
  return `${key.substring(0, 8)}...${key.substring(key.length - 4)}`
404
  }
405
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  // 复制 API Key
407
  const copyApiKey = async (key) => {
408
  try {
409
- await navigator.clipboard.writeText(key)
410
  showToast('API Key 已复制', 'success')
411
  } catch (error) {
412
  console.error('Failed to copy:', error)
413
- showToast('复制失败', 'error')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  }
415
  }
416
 
 
20
  </p>
21
  </div>
22
  </div>
23
+ <div class="flex items-center gap-2">
24
+ <button
25
+ class="flex items-center gap-2 rounded-lg border border-purple-200 bg-white/90 px-3 py-1.5 text-xs font-semibold text-purple-600 shadow-sm transition-all duration-200 hover:border-purple-300 hover:bg-purple-50 hover:text-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-200 disabled:cursor-not-allowed disabled:opacity-60 dark:border-purple-600/60 dark:bg-purple-900/20 dark:text-purple-200 dark:hover:border-purple-500 dark:hover:bg-purple-900/40 dark:hover:text-purple-100 dark:focus:ring-purple-500/40 sm:text-sm"
26
+ :disabled="loading || apiKeys.length === 0 || copyingAll"
27
+ @click="copyAllApiKeys"
28
+ >
29
+ <i
30
+ :class="[
31
+ 'text-sm sm:text-base',
32
+ copyingAll ? 'fas fa-spinner fa-spin' : 'fas fa-clipboard-list'
33
+ ]"
34
+ />
35
+ <span>复制全部 Key</span>
36
+ </button>
37
+ <button
38
+ class="flex h-9 w-9 items-center justify-center rounded-full bg-gray-100 text-gray-400 transition-colors hover:text-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:text-gray-200 sm:h-10 sm:w-10"
39
+ title="关闭"
40
+ @click="$emit('close')"
41
+ >
42
+ <i class="fas fa-times text-base sm:text-lg" />
43
+ </button>
44
+ </div>
45
  </div>
46
 
47
  <!-- 加载状态 -->
 
255
  const apiKeys = ref([])
256
  const currentPage = ref(1)
257
  const pageSize = ref(18)
258
+ const copyingAll = ref(false)
259
 
260
  // 计算属性
261
  const totalItems = computed(() => apiKeys.value.length)
 
420
  return `${key.substring(0, 8)}...${key.substring(key.length - 4)}`
421
  }
422
 
423
+ // 写入剪贴板(带回退逻辑)
424
+ const writeToClipboard = async (text) => {
425
+ const canUseClipboardApi =
426
+ typeof navigator !== 'undefined' &&
427
+ navigator.clipboard &&
428
+ typeof navigator.clipboard.writeText === 'function' &&
429
+ (typeof window === 'undefined' || window.isSecureContext !== false)
430
+
431
+ if (canUseClipboardApi) {
432
+ await navigator.clipboard.writeText(text)
433
+ return
434
+ }
435
+
436
+ if (typeof document === 'undefined') {
437
+ throw new Error('clipboard unavailable')
438
+ }
439
+
440
+ const textarea = document.createElement('textarea')
441
+ textarea.value = text
442
+ textarea.setAttribute('readonly', '')
443
+ textarea.style.position = 'fixed'
444
+ textarea.style.opacity = '0'
445
+ textarea.style.pointerEvents = 'none'
446
+ document.body.appendChild(textarea)
447
+ textarea.select()
448
+
449
+ try {
450
+ const success = document.execCommand('copy')
451
+ document.body.removeChild(textarea)
452
+ if (!success) {
453
+ throw new Error('execCommand failed')
454
+ }
455
+ } catch (error) {
456
+ document.body.removeChild(textarea)
457
+ throw error
458
+ }
459
+ }
460
+
461
  // 复制 API Key
462
  const copyApiKey = async (key) => {
463
  try {
464
+ await writeToClipboard(key)
465
  showToast('API Key 已复制', 'success')
466
  } catch (error) {
467
  console.error('Failed to copy:', error)
468
+ showToast('复制失败,请手动复制', 'error')
469
+ }
470
+ }
471
+
472
+ // 复制全部 API Key
473
+ const copyAllApiKeys = async () => {
474
+ if (!apiKeys.value.length || copyingAll.value) {
475
+ return
476
+ }
477
+
478
+ copyingAll.value = true
479
+ try {
480
+ const allKeysText = apiKeys.value.map((item) => item.key).join('\n')
481
+ await writeToClipboard(allKeysText)
482
+ showToast(`已复制 ${apiKeys.value.length} 条 API Key`, 'success')
483
+ } catch (error) {
484
+ console.error('Failed to copy all keys:', error)
485
+ showToast('复制全部 API Key 失败,请手动复制', 'error')
486
+ } finally {
487
+ copyingAll.value = false
488
  }
489
  }
490
 
web/admin-spa/src/views/AccountsView.vue CHANGED
@@ -3728,47 +3728,54 @@ const closeAccountExpiryEdit = () => {
3728
  editingExpiryAccount.value = null
3729
  }
3730
 
3731
- // 根据账户平台解析更新端点
3732
- const resolveAccountUpdateEndpoint = (account) => {
3733
- switch (account.platform) {
3734
- case 'claude':
3735
- return `/admin/claude-accounts/${account.id}`
3736
- case 'claude-console':
3737
- return `/admin/claude-console-accounts/${account.id}`
3738
- case 'bedrock':
3739
- return `/admin/bedrock-accounts/${account.id}`
3740
- case 'openai':
3741
- return `/admin/openai-accounts/${account.id}`
3742
- case 'azure_openai':
3743
- return `/admin/azure-openai-accounts/${account.id}`
3744
- case 'openai-responses':
3745
- return `/admin/openai-responses-accounts/${account.id}`
3746
- case 'ccr':
3747
- return `/admin/ccr-accounts/${account.id}`
3748
- case 'gemini':
3749
- return `/admin/gemini-accounts/${account.id}`
3750
- case 'droid':
3751
- return `/admin/droid-accounts/${account.id}`
3752
- default:
3753
- throw new Error(`Unsupported platform: ${account.platform}`)
3754
- }
3755
- }
3756
-
3757
  // 保存账户过期时间
3758
  const handleSaveAccountExpiry = async ({ accountId, expiresAt }) => {
3759
  try {
3760
- // 找到对应的账户以获取平台信息
3761
  const account = accounts.value.find((acc) => acc.id === accountId)
 
3762
  if (!account) {
3763
- showToast('账户不存在', 'error')
3764
- if (expiryEditModalRef.value) {
3765
- expiryEditModalRef.value.resetSaving()
3766
- }
3767
  return
3768
  }
3769
 
3770
- // 根据平台动态选择端点
3771
- const endpoint = resolveAccountUpdateEndpoint(account)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3772
  const data = await apiClient.put(endpoint, {
3773
  expiresAt: expiresAt || null
3774
  })
@@ -3786,7 +3793,8 @@ const handleSaveAccountExpiry = async ({ accountId, expiresAt }) => {
3786
  }
3787
  }
3788
  } catch (error) {
3789
- showToast(error.message || '更新失败', 'error')
 
3790
  // 重置保存状态
3791
  if (expiryEditModalRef.value) {
3792
  expiryEditModalRef.value.resetSaving()
@@ -3802,6 +3810,7 @@ onMounted(() => {
3802
 
3803
  <style scoped>
3804
  .table-container {
 
3805
  border-radius: 12px;
3806
  border: 1px solid rgba(0, 0, 0, 0.05);
3807
  }
@@ -3835,6 +3844,12 @@ onMounted(() => {
3835
  min-height: calc(100vh - 300px);
3836
  }
3837
 
 
 
 
 
 
 
3838
  .table-row {
3839
  transition: all 0.2s ease;
3840
  }
 
3728
  editingExpiryAccount.value = null
3729
  }
3730
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3731
  // 保存账户过期时间
3732
  const handleSaveAccountExpiry = async ({ accountId, expiresAt }) => {
3733
  try {
3734
+ // 根据账号平台选择正确的 API 端点
3735
  const account = accounts.value.find((acc) => acc.id === accountId)
3736
+
3737
  if (!account) {
3738
+ showToast('未找到账户', 'error')
 
 
 
3739
  return
3740
  }
3741
 
3742
+ // 定义每个平台的端点和参数名
3743
+ // 注意:部分平台使用 :accountId,部分使用 :id
3744
+ let endpoint = ''
3745
+ switch (account.platform) {
3746
+ case 'claude':
3747
+ case 'claude-oauth':
3748
+ endpoint = `/admin/claude-accounts/${accountId}`
3749
+ break
3750
+ case 'gemini':
3751
+ endpoint = `/admin/gemini-accounts/${accountId}`
3752
+ break
3753
+ case 'claude-console':
3754
+ endpoint = `/admin/claude-console-accounts/${accountId}`
3755
+ break
3756
+ case 'bedrock':
3757
+ endpoint = `/admin/bedrock-accounts/${accountId}`
3758
+ break
3759
+ case 'ccr':
3760
+ endpoint = `/admin/ccr-accounts/${accountId}`
3761
+ break
3762
+ case 'openai':
3763
+ endpoint = `/admin/openai-accounts/${accountId}` // 使用 :id
3764
+ break
3765
+ case 'droid':
3766
+ endpoint = `/admin/droid-accounts/${accountId}` // 使用 :id
3767
+ break
3768
+ case 'azure_openai':
3769
+ endpoint = `/admin/azure-openai-accounts/${accountId}` // 使用 :id
3770
+ break
3771
+ case 'openai-responses':
3772
+ endpoint = `/admin/openai-responses-accounts/${accountId}` // 使用 :id
3773
+ break
3774
+ default:
3775
+ showToast(`不支持的平台类型: ${account.platform}`, 'error')
3776
+ return
3777
+ }
3778
+
3779
  const data = await apiClient.put(endpoint, {
3780
  expiresAt: expiresAt || null
3781
  })
 
3793
  }
3794
  }
3795
  } catch (error) {
3796
+ console.error('更新账户过期时间失败:', error)
3797
+ showToast('更新失败', 'error')
3798
  // 重置保存状态
3799
  if (expiryEditModalRef.value) {
3800
  expiryEditModalRef.value.resetSaving()
 
3810
 
3811
  <style scoped>
3812
  .table-container {
3813
+ overflow-x: auto;
3814
  border-radius: 12px;
3815
  border: 1px solid rgba(0, 0, 0, 0.05);
3816
  }
 
3844
  min-height: calc(100vh - 300px);
3845
  }
3846
 
3847
+ .table-container {
3848
+ overflow-x: auto;
3849
+ border-radius: 12px;
3850
+ border: 1px solid rgba(0, 0, 0, 0.05);
3851
+ }
3852
+
3853
  .table-row {
3854
  transition: all 0.2s ease;
3855
  }