Upload 220 files
Browse files- VERSION +1 -1
- src/app.js +18 -1
- src/middleware/auth.js +1 -2
- src/routes/admin.js +233 -278
- src/routes/api.js +12 -30
- src/routes/droidRoutes.js +0 -43
- src/routes/openaiClaudeRoutes.js +1 -0
- src/routes/openaiRoutes.js +1 -0
- src/routes/unified.js +225 -0
- src/services/azureOpenaiAccountService.js +38 -36
- src/services/bedrockAccountService.js +43 -39
- src/services/ccrAccountService.js +30 -33
- src/services/claudeAccountService.js +9 -9
- src/services/claudeConsoleAccountService.js +30 -33
- src/services/droidAccountService.js +37 -1
- src/services/droidRelayService.js +27 -2
- src/services/droidScheduler.js +2 -4
- src/services/geminiAccountService.js +52 -41
- src/services/modelService.js +266 -0
- src/services/openaiAccountService.js +48 -42
- src/services/openaiResponsesAccountService.js +38 -32
- src/services/openaiToClaude.js +17 -2
- src/services/unifiedClaudeScheduler.js +38 -0
- src/services/unifiedOpenAIScheduler.js +22 -0
- src/utils/runtimeAddon.js +121 -0
- web/admin-spa/src/components/accounts/AccountExpiryEditModal.vue +19 -1
- web/admin-spa/src/components/accounts/AccountForm.vue +15 -3
- web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue +82 -8
- web/admin-spa/src/views/AccountsView.vue +49 -34
VERSION
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
1.1.
|
|
|
|
| 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',
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
if (!account || typeof account !== 'object') {
|
| 48 |
return account
|
| 49 |
}
|
| 50 |
|
| 51 |
-
const rawSubscription = account
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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 =
|
| 2191 |
return {
|
| 2192 |
...formattedAccount,
|
| 2193 |
groupInfos: [],
|
|
@@ -2203,8 +2236,7 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
|
|
| 2203 |
})
|
| 2204 |
)
|
| 2205 |
|
| 2206 |
-
|
| 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:
|
| 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
|
| 2365 |
-
return res.json({ success: true, data:
|
| 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 |
-
|
| 2383 |
-
(typeof
|
|
|
|
|
|
|
| 2384 |
) {
|
| 2385 |
return res.status(400).json({ error: 'Priority must be a number between 1 and 100' })
|
| 2386 |
}
|
| 2387 |
|
| 2388 |
// 验证accountType的有效性
|
| 2389 |
-
if (
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2398 |
-
!
|
| 2399 |
-
(!
|
| 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 (
|
| 2414 |
// 如果之前是分组类型,需要从所有分组中移除
|
| 2415 |
if (currentAccount.accountType === 'group') {
|
| 2416 |
await accountGroupService.removeAccountFromAllGroups(accountId)
|
| 2417 |
}
|
| 2418 |
|
| 2419 |
// 如果新类型是分组,添加到新分组
|
| 2420 |
-
if (
|
| 2421 |
// 处理多分组/单分组的兼容性
|
| 2422 |
-
if (Object.prototype.hasOwnProperty.call(
|
| 2423 |
-
if (
|
| 2424 |
// 使用多分组设置
|
| 2425 |
-
await accountGroupService.setAccountGroups(accountId,
|
| 2426 |
} else {
|
| 2427 |
// groupIds 为空数组,从所有分组中移除
|
| 2428 |
await accountGroupService.removeAccountFromAllGroups(accountId)
|
| 2429 |
}
|
| 2430 |
-
} else if (
|
| 2431 |
// 兼容单分组模式
|
| 2432 |
-
await accountGroupService.addAccountToGroup(accountId,
|
| 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 |
-
|
| 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
|
| 2777 |
-
return res.json({ success: true, data:
|
| 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 (
|
|
|
|
|
|
|
|
|
|
| 2794 |
return res.status(400).json({ error: 'Priority must be between 1 and 100' })
|
| 2795 |
}
|
| 2796 |
|
| 2797 |
// 验证accountType的有效性
|
| 2798 |
-
if (
|
|
|
|
|
|
|
|
|
|
| 2799 |
return res
|
| 2800 |
.status(400)
|
| 2801 |
.json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
|
| 2802 |
}
|
| 2803 |
|
| 2804 |
// 如果更新为分组类型,验证groupId
|
| 2805 |
-
if (
|
| 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 (
|
| 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 (
|
| 2826 |
-
if (Object.prototype.hasOwnProperty.call(
|
| 2827 |
// 如果明确提供了 groupIds 参数(包括空数组)
|
| 2828 |
-
if (
|
| 2829 |
// 设置新的多分组
|
| 2830 |
-
await accountGroupService.setAccountGroups(accountId,
|
| 2831 |
} else {
|
| 2832 |
// groupIds 为空数组,从所有分组中移除
|
| 2833 |
await accountGroupService.removeAccountFromAllGroups(accountId)
|
| 2834 |
}
|
| 2835 |
-
} else if (
|
| 2836 |
// 向后兼容:仅当没有 groupIds 但有 groupId 时使用单分组逻辑
|
| 2837 |
-
await accountGroupService.addAccountToGroup(accountId,
|
| 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 |
-
...
|
| 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 |
-
|
| 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
|
| 3203 |
-
return res.json({ success: true, data:
|
| 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 (
|
|
|
|
|
|
|
|
|
|
| 3218 |
return res.status(400).json({ error: 'Priority must be between 1 and 100' })
|
| 3219 |
}
|
| 3220 |
|
| 3221 |
// 验证accountType的有效性
|
| 3222 |
-
if (
|
|
|
|
|
|
|
|
|
|
| 3223 |
return res
|
| 3224 |
.status(400)
|
| 3225 |
.json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
|
| 3226 |
}
|
| 3227 |
|
| 3228 |
// 如果更新为分组类型,验证groupId
|
| 3229 |
-
if (
|
| 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 (
|
| 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 (
|
| 3250 |
-
if (Object.prototype.hasOwnProperty.call(
|
| 3251 |
// 如果明确提供了 groupIds 参数(包括空数组)
|
| 3252 |
-
if (
|
| 3253 |
// 设置新的多分组
|
| 3254 |
-
await accountGroupService.setAccountGroups(accountId,
|
| 3255 |
} else {
|
| 3256 |
// groupIds 为空数组,从所有分组中移除
|
| 3257 |
await accountGroupService.removeAccountFromAllGroups(accountId)
|
| 3258 |
}
|
| 3259 |
-
} else if (
|
| 3260 |
// 向后兼容:仅当没有 groupIds 但有 groupId 时使用单分组逻辑
|
| 3261 |
-
await accountGroupService.addAccountToGroup(accountId,
|
| 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 |
-
...
|
| 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 |
-
|
| 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
|
| 3604 |
-
return res.json({ success: true, data:
|
| 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 (
|
|
|
|
|
|
|
|
|
|
| 3621 |
return res.status(400).json({ error: 'Priority must be between 1 and 100' })
|
| 3622 |
}
|
| 3623 |
|
| 3624 |
// 验证accountType的有效性
|
| 3625 |
-
if (
|
| 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 |
-
|
| 3634 |
-
!['default', 'access_key', 'bearer_token'].includes(
|
| 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 |
-
...
|
| 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 |
-
|
| 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
|
| 4063 |
-
return res.json({ success: true, data:
|
| 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 (
|
| 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 (
|
| 4105 |
-
if (Object.prototype.hasOwnProperty.call(
|
| 4106 |
// 如果明确提供了 groupIds 参数(包括空数组)
|
| 4107 |
-
if (
|
| 4108 |
// 设置新的多分组
|
| 4109 |
-
await accountGroupService.setAccountGroups(accountId,
|
| 4110 |
} else {
|
| 4111 |
// groupIds 为空数组,从所有分组中移除
|
| 4112 |
await accountGroupService.removeAccountFromAllGroups(accountId)
|
| 4113 |
}
|
| 4114 |
-
} else if (
|
| 4115 |
// 向后兼容:仅当没有 groupIds 但有 groupId 时使用单分组逻辑
|
| 4116 |
-
await accountGroupService.addAccountToGroup(accountId,
|
| 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 |
-
|
| 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 =
|
| 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 =
|
| 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:
|
| 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:
|
| 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:
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7491 |
|
| 7492 |
// 验证accountType的有效性
|
| 7493 |
-
if (
|
|
|
|
|
|
|
|
|
|
| 7494 |
return res
|
| 7495 |
.status(400)
|
| 7496 |
.json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
|
| 7497 |
}
|
| 7498 |
|
| 7499 |
// 如果更新为分组类型,验证groupId
|
| 7500 |
-
if (
|
| 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 (
|
| 7512 |
// 先更新 token 信息
|
| 7513 |
const tempUpdateData = {}
|
| 7514 |
-
if (
|
| 7515 |
-
tempUpdateData.refreshToken =
|
| 7516 |
}
|
| 7517 |
-
if (
|
| 7518 |
-
tempUpdateData.accessToken =
|
| 7519 |
}
|
| 7520 |
// 更新代理配置(如果有)
|
| 7521 |
-
if (
|
| 7522 |
-
tempUpdateData.proxy =
|
| 7523 |
}
|
| 7524 |
|
| 7525 |
// 临时更新账户以测试新的 token
|
|
@@ -7595,7 +7604,7 @@ router.put('/openai-accounts/:id', authenticateAdmin, async (req, res) => {
|
|
| 7595 |
}
|
| 7596 |
|
| 7597 |
// 处理分组的变更
|
| 7598 |
-
if (
|
| 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 (
|
| 7608 |
-
await accountGroupService.addAccountToGroup(id,
|
| 7609 |
}
|
| 7610 |
}
|
| 7611 |
|
| 7612 |
// 准备更新数据
|
| 7613 |
-
const updateData = { ...
|
| 7614 |
|
| 7615 |
// 处理敏感数据加密
|
| 7616 |
-
if (
|
| 7617 |
-
updateData.openaiOauth =
|
| 7618 |
// 编辑时不允许直接输入 ID Token,只能通过刷新获取
|
| 7619 |
-
if (
|
| 7620 |
-
updateData.accessToken =
|
| 7621 |
}
|
| 7622 |
-
if (
|
| 7623 |
-
updateData.refreshToken =
|
| 7624 |
}
|
| 7625 |
-
if (
|
| 7626 |
updateData.expiresAt = new Date(
|
| 7627 |
-
Date.now() +
|
| 7628 |
).toISOString()
|
| 7629 |
}
|
| 7630 |
}
|
| 7631 |
|
| 7632 |
// 更新账户信息
|
| 7633 |
-
if (
|
| 7634 |
-
updateData.accountId =
|
| 7635 |
-
updateData.chatgptUserId =
|
|
|
|
| 7636 |
updateData.organizationId =
|
| 7637 |
-
|
| 7638 |
updateData.organizationRole =
|
| 7639 |
-
|
| 7640 |
updateData.organizationTitle =
|
| 7641 |
-
|
| 7642 |
-
updateData.planType =
|
| 7643 |
-
updateData.email =
|
| 7644 |
updateData.emailVerified =
|
| 7645 |
-
|
| 7646 |
-
?
|
| 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 |
-
|
| 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:
|
| 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 |
-
...
|
| 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:
|
| 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:
|
| 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 |
-
//
|
| 8061 |
-
const mappedUpdates =
|
| 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:
|
| 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 |
-
|
| 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
|
| 8421 |
-
res.json({ success: true, data:
|
| 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 (
|
| 8439 |
-
const priority = parseInt(
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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
|
| 8918 |
-
return res.json({ success: true, data:
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 8953 |
-
const hasGroupIdField = Object.prototype.hasOwnProperty.call(
|
| 8954 |
const targetAccountType = rawAccountType || currentAccount.accountType || 'shared'
|
| 8955 |
|
| 8956 |
-
delete
|
| 8957 |
-
delete
|
| 8958 |
|
| 8959 |
if (rawAccountType) {
|
| 8960 |
-
|
| 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 |
-
|
| 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
|
| 726 |
router.get('/v1/models', authenticateApiKey, async (req, res) => {
|
| 727 |
try {
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 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:
|
| 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 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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'
|
| 57 |
-
subscriptionExpiresAt = null
|
| 58 |
} = options
|
| 59 |
|
| 60 |
const accountId = uuidv4()
|
|
@@ -70,7 +56,11 @@ class BedrockAccountService {
|
|
| 70 |
priority,
|
| 71 |
schedulable,
|
| 72 |
credentialType,
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 170 |
-
|
| 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 |
-
|
| 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'
|
| 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 |
-
|
| 315 |
-
|
| 316 |
-
|
| 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} -
|
| 793 |
*/
|
| 794 |
-
|
| 795 |
if (!account.subscriptionExpiresAt) {
|
| 796 |
-
return
|
| 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
|
| 807 |
}
|
| 808 |
|
| 809 |
-
return
|
| 810 |
}
|
| 811 |
|
| 812 |
// 🎯 智能选择可用账户(支持sticky会话和模型过滤)
|
|
@@ -819,7 +819,7 @@ class ClaudeAccountService {
|
|
| 819 |
account.isActive === 'true' &&
|
| 820 |
account.status !== 'error' &&
|
| 821 |
account.schedulable !== 'false' &&
|
| 822 |
-
this.
|
| 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.
|
| 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.
|
| 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'
|
| 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 |
-
|
| 345 |
-
|
| 346 |
-
|
| 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.
|
| 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 |
-
|
| 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
|
| 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 |
-
//
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 577 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 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 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 4847 |
} catch (error) {
|
| 4848 |
-
console.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 |
-
<
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 410 |
showToast('API Key 已复制', 'success')
|
| 411 |
} catch (error) {
|
| 412 |
console.error('Failed to copy:', error)
|
| 413 |
-
showToast('
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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('
|
| 3764 |
-
if (expiryEditModalRef.value) {
|
| 3765 |
-
expiryEditModalRef.value.resetSaving()
|
| 3766 |
-
}
|
| 3767 |
return
|
| 3768 |
}
|
| 3769 |
|
| 3770 |
-
//
|
| 3771 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
}
|