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,
|
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=强制启用
|
||||||
})
|
})
|
||||||
|
|
||||||
// 如果是分组类型,将账户添加到分组
|
// 如果是分组类型,将账户添加到分组
|
||||||
|
|||||||
@@ -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') {
|
||||||
// 处理订阅信息更新
|
// 处理订阅信息更新
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 || '',
|
||||||
|
|||||||
Reference in New Issue
Block a user