diff --git a/src/routes/admin/claudeAccounts.js b/src/routes/admin/claudeAccounts.js index da8db547..52791374 100644 --- a/src/routes/admin/claudeAccounts.js +++ b/src/routes/admin/claudeAccounts.js @@ -584,7 +584,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => { useUnifiedClientId, unifiedClientId, expiresAt, - extInfo + extInfo, + maxConcurrency } = req.body if (!name) { @@ -629,7 +630,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => { useUnifiedClientId: useUnifiedClientId === true, // 默认为false unifiedClientId: unifiedClientId || '', // 统一的客户端标识 expiresAt: expiresAt || null, // 账户订阅到期时间 - extInfo: extInfo || null + extInfo: extInfo || null, + maxConcurrency: maxConcurrency || 0 // 账户级串行队列:0=使用全局配置,>0=强制启用 }) // 如果是分组类型,将账户添加到分组 diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index 77630364..35ce9cff 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -91,7 +91,8 @@ class ClaudeAccountService { useUnifiedClientId = false, // 是否使用统一的客户端标识 unifiedClientId = '', // 统一的客户端标识 expiresAt = null, // 账户订阅到期时间 - extInfo = null // 额外扩展信息 + extInfo = null, // 额外扩展信息 + maxConcurrency = 0 // 账户级用户消息串行队列:0=使用全局配置,>0=强制启用串行 } = options const accountId = uuidv4() @@ -136,7 +137,9 @@ class ClaudeAccountService { // 账户订阅到期时间 subscriptionExpiresAt: expiresAt || '', // 扩展信息 - extInfo: normalizedExtInfo ? JSON.stringify(normalizedExtInfo) : '' + extInfo: normalizedExtInfo ? JSON.stringify(normalizedExtInfo) : '', + // 账户级用户消息串行队列限制 + maxConcurrency: maxConcurrency.toString() } } else { // 兼容旧格式 @@ -168,7 +171,9 @@ class ClaudeAccountService { // 账户订阅到期时间 subscriptionExpiresAt: expiresAt || '', // 扩展信息 - extInfo: normalizedExtInfo ? JSON.stringify(normalizedExtInfo) : '' + extInfo: normalizedExtInfo ? JSON.stringify(normalizedExtInfo) : '', + // 账户级用户消息串行队列限制 + maxConcurrency: maxConcurrency.toString() } } @@ -574,7 +579,9 @@ class ClaudeAccountService { // 添加停止原因 stoppedReason: account.stoppedReason || null, // 扩展信息 - extInfo: parsedExtInfo + extInfo: parsedExtInfo, + // 账户级用户消息串行队列限制 + maxConcurrency: parseInt(account.maxConcurrency || '0', 10) } }) ) @@ -666,7 +673,8 @@ class ClaudeAccountService { 'useUnifiedClientId', 'unifiedClientId', 'subscriptionExpiresAt', - 'extInfo' + 'extInfo', + 'maxConcurrency' ] const updatedData = { ...accountData } let shouldClearAutoStopFields = false @@ -681,7 +689,7 @@ class ClaudeAccountService { updatedData[field] = this._encryptSensitiveData(value) } else if (field === 'proxy') { updatedData[field] = value ? JSON.stringify(value) : '' - } else if (field === 'priority') { + } else if (field === 'priority' || field === 'maxConcurrency') { updatedData[field] = value.toString() } else if (field === 'subscriptionInfo') { // 处理订阅信息更新 diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js index 001ee313..40c6103b 100644 --- a/src/services/claudeRelayService.js +++ b/src/services/claudeRelayService.js @@ -210,7 +210,17 @@ class ClaudeRelayService { logger.error('❌ accountId missing for queue lock in relayRequest') throw new Error('accountId missing for queue lock') } - const queueResult = await userMessageQueueService.acquireQueueLock(accountId) + // 获取账户信息以检查账户级串行队列配置 + const accountForQueue = await claudeAccountService.getAccount(accountId) + const accountConfig = accountForQueue + ? { maxConcurrency: parseInt(accountForQueue.maxConcurrency || '0', 10) } + : null + const queueResult = await userMessageQueueService.acquireQueueLock( + accountId, + null, + null, + accountConfig + ) if (!queueResult.acquired && !queueResult.skipped) { // 区分 Redis 后端错误和队列超时 const isBackendError = queueResult.error === 'queue_backend_error' @@ -1314,7 +1324,17 @@ class ClaudeRelayService { logger.error('❌ accountId missing for queue lock in relayStreamRequestWithUsageCapture') throw new Error('accountId missing for queue lock') } - const queueResult = await userMessageQueueService.acquireQueueLock(accountId) + // 获取账户信息以检查账户级串行队列配置 + const accountForQueue = await claudeAccountService.getAccount(accountId) + const accountConfig = accountForQueue + ? { maxConcurrency: parseInt(accountForQueue.maxConcurrency || '0', 10) } + : null + const queueResult = await userMessageQueueService.acquireQueueLock( + accountId, + null, + null, + accountConfig + ) if (!queueResult.acquired && !queueResult.skipped) { // 区分 Redis 后端错误和队列超时 const isBackendError = queueResult.error === 'queue_backend_error' diff --git a/src/services/userMessageQueueService.js b/src/services/userMessageQueueService.js index e35a9f64..2b4784a2 100644 --- a/src/services/userMessageQueueService.js +++ b/src/services/userMessageQueueService.js @@ -121,12 +121,23 @@ class UserMessageQueueService { * @param {string} accountId - 账户ID * @param {string} requestId - 请求ID(可选,会自动生成) * @param {number} timeoutMs - 超时时间(可选,使用配置默认值) + * @param {Object} accountConfig - 账户级配置(可选),优先级高于全局配置 + * @param {number} accountConfig.maxConcurrency - 账户级串行队列开关:>0启用,=0使用全局配置 * @returns {Promise<{acquired: boolean, requestId: string, error?: string}>} */ - async acquireQueueLock(accountId, requestId = null, timeoutMs = null) { + async acquireQueueLock(accountId, requestId = null, timeoutMs = null, accountConfig = null) { const cfg = await this.getConfig() - if (!cfg.enabled) { + // 账户级配置优先:maxConcurrency > 0 时强制启用,忽略全局开关 + let queueEnabled = cfg.enabled + if (accountConfig && accountConfig.maxConcurrency > 0) { + queueEnabled = true + logger.debug( + `📬 User message queue: account-level queue enabled for account ${accountId} (maxConcurrency=${accountConfig.maxConcurrency})` + ) + } + + if (!queueEnabled) { return { acquired: true, requestId: requestId || uuidv4(), skipped: true } } diff --git a/web/admin-spa/src/components/accounts/AccountForm.vue b/web/admin-spa/src/components/accounts/AccountForm.vue index 1a36f4c3..f23a3b5f 100644 --- a/web/admin-spa/src/components/accounts/AccountForm.vue +++ b/web/admin-spa/src/components/accounts/AccountForm.vue @@ -1632,6 +1632,25 @@ + +
+ +
+
+ +
+ +
+