mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 支持claude单账户开启串行队列
This commit is contained in:
@@ -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=强制启用
|
||||
})
|
||||
|
||||
// 如果是分组类型,将账户添加到分组
|
||||
|
||||
@@ -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') {
|
||||
// 处理订阅信息更新
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
|
||||
@@ -1632,6 +1632,25 @@
|
||||
</label>
|
||||
</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 版本配置 -->
|
||||
<div v-if="form.platform === 'claude'" class="mt-4">
|
||||
<label class="flex items-start">
|
||||
@@ -2615,6 +2634,25 @@
|
||||
</label>
|
||||
</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 版本配置(编辑模式) -->
|
||||
<div v-if="form.platform === 'claude'" class="mt-4">
|
||||
<label class="flex items-start">
|
||||
@@ -3949,6 +3987,7 @@ const form = ref({
|
||||
useUnifiedUserAgent: props.account?.useUnifiedUserAgent || false, // 使用统一Claude Code版本
|
||||
useUnifiedClientId: props.account?.useUnifiedClientId || false, // 使用统一的客户端标识
|
||||
unifiedClientId: props.account?.unifiedClientId || '', // 统一的客户端标识
|
||||
serialQueueEnabled: (props.account?.maxConcurrency || 0) > 0, // 账户级串行队列开关
|
||||
groupId: '',
|
||||
groupIds: [],
|
||||
projectId: props.account?.projectId || '',
|
||||
@@ -4538,6 +4577,7 @@ const buildClaudeAccountData = (tokenInfo, accountName, clientId) => {
|
||||
useUnifiedUserAgent: form.value.useUnifiedUserAgent || false,
|
||||
useUnifiedClientId: form.value.useUnifiedClientId || false,
|
||||
unifiedClientId: clientId,
|
||||
maxConcurrency: form.value.serialQueueEnabled ? 1 : 0,
|
||||
subscriptionInfo: {
|
||||
accountType: 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.useUnifiedClientId = form.value.useUnifiedClientId || false
|
||||
data.unifiedClientId = form.value.unifiedClientId || ''
|
||||
data.maxConcurrency = form.value.serialQueueEnabled ? 1 : 0
|
||||
// 添加订阅类型信息
|
||||
data.subscriptionInfo = {
|
||||
accountType: form.value.subscriptionType || 'claude_max',
|
||||
@@ -4998,6 +5039,7 @@ const createAccount = async () => {
|
||||
data.useUnifiedUserAgent = form.value.useUnifiedUserAgent || false
|
||||
data.useUnifiedClientId = form.value.useUnifiedClientId || false
|
||||
data.unifiedClientId = form.value.unifiedClientId || ''
|
||||
data.maxConcurrency = form.value.serialQueueEnabled ? 1 : 0
|
||||
// 添加订阅类型信息
|
||||
data.subscriptionInfo = {
|
||||
accountType: form.value.subscriptionType || 'claude_max',
|
||||
@@ -5388,6 +5430,7 @@ const updateAccount = async () => {
|
||||
data.useUnifiedUserAgent = form.value.useUnifiedUserAgent || false
|
||||
data.useUnifiedClientId = form.value.useUnifiedClientId || false
|
||||
data.unifiedClientId = form.value.unifiedClientId || ''
|
||||
data.maxConcurrency = form.value.serialQueueEnabled ? 1 : 0
|
||||
// 更新订阅类型信息
|
||||
data.subscriptionInfo = {
|
||||
accountType: form.value.subscriptionType || 'claude_max',
|
||||
@@ -5991,6 +6034,7 @@ watch(
|
||||
useUnifiedUserAgent: newAccount.useUnifiedUserAgent || false,
|
||||
useUnifiedClientId: newAccount.useUnifiedClientId || false,
|
||||
unifiedClientId: newAccount.unifiedClientId || '',
|
||||
serialQueueEnabled: (newAccount.maxConcurrency || 0) > 0,
|
||||
groupId: groupId,
|
||||
groupIds: [],
|
||||
projectId: newAccount.projectId || '',
|
||||
|
||||
Reference in New Issue
Block a user