feat: 支持claude单账户开启串行队列

This commit is contained in:
shaw
2025-12-19 22:29:57 +08:00
parent fa2fc2fb16
commit 638d2ff189
5 changed files with 97 additions and 12 deletions

View File

@@ -584,7 +584,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
useUnifiedClientId, useUnifiedClientId,
unifiedClientId, unifiedClientId,
expiresAt, expiresAt,
extInfo extInfo,
maxConcurrency
} = req.body } = req.body
if (!name) { if (!name) {
@@ -629,7 +630,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
useUnifiedClientId: useUnifiedClientId === true, // 默认为false useUnifiedClientId: useUnifiedClientId === true, // 默认为false
unifiedClientId: unifiedClientId || '', // 统一的客户端标识 unifiedClientId: unifiedClientId || '', // 统一的客户端标识
expiresAt: expiresAt || null, // 账户订阅到期时间 expiresAt: expiresAt || null, // 账户订阅到期时间
extInfo: extInfo || null extInfo: extInfo || null,
maxConcurrency: maxConcurrency || 0 // 账户级串行队列0=使用全局配置,>0=强制启用
}) })
// 如果是分组类型,将账户添加到分组 // 如果是分组类型,将账户添加到分组

View File

@@ -91,7 +91,8 @@ class ClaudeAccountService {
useUnifiedClientId = false, // 是否使用统一的客户端标识 useUnifiedClientId = false, // 是否使用统一的客户端标识
unifiedClientId = '', // 统一的客户端标识 unifiedClientId = '', // 统一的客户端标识
expiresAt = null, // 账户订阅到期时间 expiresAt = null, // 账户订阅到期时间
extInfo = null // 额外扩展信息 extInfo = null, // 额外扩展信息
maxConcurrency = 0 // 账户级用户消息串行队列0=使用全局配置,>0=强制启用串行
} = options } = options
const accountId = uuidv4() const accountId = uuidv4()
@@ -136,7 +137,9 @@ class ClaudeAccountService {
// 账户订阅到期时间 // 账户订阅到期时间
subscriptionExpiresAt: expiresAt || '', subscriptionExpiresAt: expiresAt || '',
// 扩展信息 // 扩展信息
extInfo: normalizedExtInfo ? JSON.stringify(normalizedExtInfo) : '' extInfo: normalizedExtInfo ? JSON.stringify(normalizedExtInfo) : '',
// 账户级用户消息串行队列限制
maxConcurrency: maxConcurrency.toString()
} }
} else { } else {
// 兼容旧格式 // 兼容旧格式
@@ -168,7 +171,9 @@ class ClaudeAccountService {
// 账户订阅到期时间 // 账户订阅到期时间
subscriptionExpiresAt: expiresAt || '', 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, stoppedReason: account.stoppedReason || null,
// 扩展信息 // 扩展信息
extInfo: parsedExtInfo extInfo: parsedExtInfo,
// 账户级用户消息串行队列限制
maxConcurrency: parseInt(account.maxConcurrency || '0', 10)
} }
}) })
) )
@@ -666,7 +673,8 @@ class ClaudeAccountService {
'useUnifiedClientId', 'useUnifiedClientId',
'unifiedClientId', 'unifiedClientId',
'subscriptionExpiresAt', 'subscriptionExpiresAt',
'extInfo' 'extInfo',
'maxConcurrency'
] ]
const updatedData = { ...accountData } const updatedData = { ...accountData }
let shouldClearAutoStopFields = false let shouldClearAutoStopFields = false
@@ -681,7 +689,7 @@ class ClaudeAccountService {
updatedData[field] = this._encryptSensitiveData(value) updatedData[field] = this._encryptSensitiveData(value)
} else if (field === 'proxy') { } else if (field === 'proxy') {
updatedData[field] = value ? JSON.stringify(value) : '' updatedData[field] = value ? JSON.stringify(value) : ''
} else if (field === 'priority') { } else if (field === 'priority' || field === 'maxConcurrency') {
updatedData[field] = value.toString() updatedData[field] = value.toString()
} else if (field === 'subscriptionInfo') { } else if (field === 'subscriptionInfo') {
// 处理订阅信息更新 // 处理订阅信息更新

View File

@@ -210,7 +210,17 @@ class ClaudeRelayService {
logger.error('❌ accountId missing for queue lock in relayRequest') logger.error('❌ accountId missing for queue lock in relayRequest')
throw new Error('accountId missing for queue lock') 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) { if (!queueResult.acquired && !queueResult.skipped) {
// 区分 Redis 后端错误和队列超时 // 区分 Redis 后端错误和队列超时
const isBackendError = queueResult.error === 'queue_backend_error' const isBackendError = queueResult.error === 'queue_backend_error'
@@ -1314,7 +1324,17 @@ class ClaudeRelayService {
logger.error('❌ accountId missing for queue lock in relayStreamRequestWithUsageCapture') logger.error('❌ accountId missing for queue lock in relayStreamRequestWithUsageCapture')
throw new Error('accountId missing for queue lock') 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) { if (!queueResult.acquired && !queueResult.skipped) {
// 区分 Redis 后端错误和队列超时 // 区分 Redis 后端错误和队列超时
const isBackendError = queueResult.error === 'queue_backend_error' const isBackendError = queueResult.error === 'queue_backend_error'

View File

@@ -121,12 +121,23 @@ class UserMessageQueueService {
* @param {string} accountId - 账户ID * @param {string} accountId - 账户ID
* @param {string} requestId - 请求ID可选会自动生成 * @param {string} requestId - 请求ID可选会自动生成
* @param {number} timeoutMs - 超时时间(可选,使用配置默认值) * @param {number} timeoutMs - 超时时间(可选,使用配置默认值)
* @param {Object} accountConfig - 账户级配置(可选),优先级高于全局配置
* @param {number} accountConfig.maxConcurrency - 账户级串行队列开关:>0启用=0使用全局配置
* @returns {Promise<{acquired: boolean, requestId: string, error?: string}>} * @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() 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 } return { acquired: true, requestId: requestId || uuidv4(), skipped: true }
} }

View File

@@ -1632,6 +1632,25 @@
</label> </label>
</div> </div>
<!-- Claude 账户级串行队列开关 -->
<div v-if="form.platform === 'claude'" class="mt-4">
<label class="flex items-start">
<input
v-model="form.serialQueueEnabled"
class="mt-1 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
type="checkbox"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
启用账户级串行队列
</span>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
开启后强制该账户的用户消息串行处理,忽略全局串行队列设置。适用于并发限制较低的账户。
</p>
</div>
</label>
</div>
<!-- Claude User-Agent 版本配置 --> <!-- Claude User-Agent 版本配置 -->
<div v-if="form.platform === 'claude'" class="mt-4"> <div v-if="form.platform === 'claude'" class="mt-4">
<label class="flex items-start"> <label class="flex items-start">
@@ -2615,6 +2634,25 @@
</label> </label>
</div> </div>
<!-- Claude 账户级串行队列开关(编辑模式) -->
<div v-if="form.platform === 'claude'" class="mt-4">
<label class="flex items-start">
<input
v-model="form.serialQueueEnabled"
class="mt-1 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
type="checkbox"
/>
<div class="ml-3">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
启用账户级串行队列
</span>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
开启后强制该账户的用户消息串行处理,忽略全局串行队列设置。适用于并发限制较低的账户。
</p>
</div>
</label>
</div>
<!-- Claude User-Agent 版本配置(编辑模式) --> <!-- Claude User-Agent 版本配置(编辑模式) -->
<div v-if="form.platform === 'claude'" class="mt-4"> <div v-if="form.platform === 'claude'" class="mt-4">
<label class="flex items-start"> <label class="flex items-start">
@@ -3949,6 +3987,7 @@ const form = ref({
useUnifiedUserAgent: props.account?.useUnifiedUserAgent || false, // 使用统一Claude Code版本 useUnifiedUserAgent: props.account?.useUnifiedUserAgent || false, // 使用统一Claude Code版本
useUnifiedClientId: props.account?.useUnifiedClientId || false, // 使用统一的客户端标识 useUnifiedClientId: props.account?.useUnifiedClientId || false, // 使用统一的客户端标识
unifiedClientId: props.account?.unifiedClientId || '', // 统一的客户端标识 unifiedClientId: props.account?.unifiedClientId || '', // 统一的客户端标识
serialQueueEnabled: (props.account?.maxConcurrency || 0) > 0, // 账户级串行队列开关
groupId: '', groupId: '',
groupIds: [], groupIds: [],
projectId: props.account?.projectId || '', projectId: props.account?.projectId || '',
@@ -4538,6 +4577,7 @@ const buildClaudeAccountData = (tokenInfo, accountName, clientId) => {
useUnifiedUserAgent: form.value.useUnifiedUserAgent || false, useUnifiedUserAgent: form.value.useUnifiedUserAgent || false,
useUnifiedClientId: form.value.useUnifiedClientId || false, useUnifiedClientId: form.value.useUnifiedClientId || false,
unifiedClientId: clientId, unifiedClientId: clientId,
maxConcurrency: form.value.serialQueueEnabled ? 1 : 0,
subscriptionInfo: { subscriptionInfo: {
accountType: form.value.subscriptionType || 'claude_max', accountType: form.value.subscriptionType || 'claude_max',
hasClaudeMax: form.value.subscriptionType === 'claude_max', hasClaudeMax: form.value.subscriptionType === 'claude_max',
@@ -4675,6 +4715,7 @@ const handleOAuthSuccess = async (tokenInfoOrList) => {
data.useUnifiedUserAgent = form.value.useUnifiedUserAgent || false data.useUnifiedUserAgent = form.value.useUnifiedUserAgent || false
data.useUnifiedClientId = form.value.useUnifiedClientId || false data.useUnifiedClientId = form.value.useUnifiedClientId || false
data.unifiedClientId = form.value.unifiedClientId || '' data.unifiedClientId = form.value.unifiedClientId || ''
data.maxConcurrency = form.value.serialQueueEnabled ? 1 : 0
// 添加订阅类型信息 // 添加订阅类型信息
data.subscriptionInfo = { data.subscriptionInfo = {
accountType: form.value.subscriptionType || 'claude_max', accountType: form.value.subscriptionType || 'claude_max',
@@ -4998,6 +5039,7 @@ const createAccount = async () => {
data.useUnifiedUserAgent = form.value.useUnifiedUserAgent || false data.useUnifiedUserAgent = form.value.useUnifiedUserAgent || false
data.useUnifiedClientId = form.value.useUnifiedClientId || false data.useUnifiedClientId = form.value.useUnifiedClientId || false
data.unifiedClientId = form.value.unifiedClientId || '' data.unifiedClientId = form.value.unifiedClientId || ''
data.maxConcurrency = form.value.serialQueueEnabled ? 1 : 0
// 添加订阅类型信息 // 添加订阅类型信息
data.subscriptionInfo = { data.subscriptionInfo = {
accountType: form.value.subscriptionType || 'claude_max', accountType: form.value.subscriptionType || 'claude_max',
@@ -5388,6 +5430,7 @@ const updateAccount = async () => {
data.useUnifiedUserAgent = form.value.useUnifiedUserAgent || false data.useUnifiedUserAgent = form.value.useUnifiedUserAgent || false
data.useUnifiedClientId = form.value.useUnifiedClientId || false data.useUnifiedClientId = form.value.useUnifiedClientId || false
data.unifiedClientId = form.value.unifiedClientId || '' data.unifiedClientId = form.value.unifiedClientId || ''
data.maxConcurrency = form.value.serialQueueEnabled ? 1 : 0
// 更新订阅类型信息 // 更新订阅类型信息
data.subscriptionInfo = { data.subscriptionInfo = {
accountType: form.value.subscriptionType || 'claude_max', accountType: form.value.subscriptionType || 'claude_max',
@@ -5991,6 +6034,7 @@ watch(
useUnifiedUserAgent: newAccount.useUnifiedUserAgent || false, useUnifiedUserAgent: newAccount.useUnifiedUserAgent || false,
useUnifiedClientId: newAccount.useUnifiedClientId || false, useUnifiedClientId: newAccount.useUnifiedClientId || false,
unifiedClientId: newAccount.unifiedClientId || '', unifiedClientId: newAccount.unifiedClientId || '',
serialQueueEnabled: (newAccount.maxConcurrency || 0) > 0,
groupId: groupId, groupId: groupId,
groupIds: [], groupIds: [],
projectId: newAccount.projectId || '', projectId: newAccount.projectId || '',