feat: 为 Claude Console 账户添加并发控制机制

实现了完整的 Claude Console 账户并发任务数控制功能,防止单账户过载,提升服务稳定性。

  **核心功能**

  - 🔒 **原子性并发控制**: 基于 Redis Sorted Set 实现的抢占式并发槽位管理,防止竞态条件
  - 🔄 **自动租约刷新**: 流式请求每 5 分钟自动刷新租约,防止长连接租约过期
  - 🚨 **智能降级处理**: 并发满额时自动清理粘性会话并重试其他账户(最多 1 次)
  - 🎯 **专用错误码**: 引入 `CONSOLE_ACCOUNT_CONCURRENCY_FULL` 错误码,区分并发限制和其他错误
  - 📊 **批量性能优化**: 调度器使用 Promise.all 并行查询账户并发数,减少 Redis 往返

  **后端实现**

  1. **Redis 并发控制方法** (src/models/redis.js)
     - `incrConsoleAccountConcurrency()`: 增加并发计数(带租约)
     - `decrConsoleAccountConcurrency()`: 释放并发槽位
     - `refreshConsoleAccountConcurrencyLease()`: 刷新租约(流式请求)
     - `getConsoleAccountConcurrency()`: 查询当前并发数

  2. **账户服务增强** (src/services/claudeConsoleAccountService.js)
     - 添加 `maxConcurrentTasks` 字段(默认 0 表示无限制)
     - 获取账户时自动查询实时并发数 (`activeTaskCount`)
     - 支持更新并发限制配置

  3. **转发服务并发保护** (src/services/claudeConsoleRelayService.js)
     - 请求前原子性抢占槽位,超限则立即回滚并抛出专用错误
     - 流式请求启动定时器每 5 分钟刷新租约
     - `finally` 块确保槽位释放(即使发生异常)
     - 为每个请求分配唯一 `requestId` 用于并发追踪

  4. **统一调度器优化** (src/services/unifiedClaudeScheduler.js)
     - 获取可用账户时批量查询并发数(Promise.all 并行)
     - 预检查并发限制,避免选择已满的账户
     - 检查分组成员时也验证并发状态
     - 所有账户并发满额时抛出专用错误码

  5. **API 路由降级处理** (src/routes/api.js)
     - 捕获 `CONSOLE_ACCOUNT_CONCURRENCY_FULL` 错误
     - 自动清理粘性会话映射并重试(最多 1 次)
     - 重试失败返回 503 错误和友好提示
     - count_tokens 端点也支持并发满额重试

  6. **管理端点验证** (src/routes/admin.js)
     - 创建/更新账户时验证 `maxConcurrentTasks` 为非负整数
     - 支持前端传入并发限制配置

  **前端实现**

  1. **表单字段** (web/admin-spa/src/components/accounts/AccountForm.vue)
     - 添加"最大并发任务数"输入框(创建和编辑模式)
     - 支持占位符提示"0 表示不限制"
     - 表单数据自动映射到后端 API

  2. **实时监控** (web/admin-spa/src/views/AccountsView.vue)
     - 账户列表显示并发状态进度条和百分比
     - 颜色编码:绿色(<80%)、黄色(80%-100%)、红色(100%)
     - 显示"X / Y"格式的并发数(如"2 / 5")
     - 未配置限制时显示"并发无限制"徽章
This commit is contained in:
sususu98
2025-10-21 13:43:57 +08:00
parent b61a3103e9
commit 1458d609ca
8 changed files with 706 additions and 119 deletions

View File

@@ -1142,6 +1142,23 @@
</div>
</div>
<!-- 并发控制字段 -->
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
最大并发任务数
</label>
<input
v-model.number="form.maxConcurrentTasks"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
min="0"
placeholder="0 表示不限制"
type="number"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
限制该账户的并发请求数量0 表示不限制
</p>
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>模型限制 (可选)</label
@@ -2540,6 +2557,23 @@
</div>
</div>
<!-- 并发控制字段(编辑模式)-->
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
最大并发任务数
</label>
<input
v-model.number="form.maxConcurrentTasks"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
min="0"
placeholder="0 表示不限制"
type="number"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
限制该账户的并发请求数量0 表示不限制
</p>
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>模型限制 (可选)</label
@@ -2873,6 +2907,23 @@
/>
</div>
</div>
<!-- 并发控制字段 -->
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
最大并发任务数
</label>
<input
v-model.number="form.maxConcurrentTasks"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
min="0"
placeholder="0 表示不限制"
type="number"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
限制该账户的并发请求数量0 表示不限制
</p>
</div>
</div>
<!-- Bedrock 特定字段(编辑模式)-->
@@ -3542,6 +3593,8 @@ const form = ref({
dailyQuota: props.account?.dailyQuota || 0,
dailyUsage: props.account?.dailyUsage || 0,
quotaResetTime: props.account?.quotaResetTime || '00:00',
// 并发控制字段
maxConcurrentTasks: props.account?.maxConcurrentTasks || 0,
// Bedrock 特定字段
accessKeyId: props.account?.accessKeyId || '',
secretAccessKey: props.account?.secretAccessKey || '',
@@ -4436,6 +4489,8 @@ const createAccount = async () => {
// 额度管理字段
data.dailyQuota = form.value.dailyQuota || 0
data.quotaResetTime = form.value.quotaResetTime || '00:00'
// 并发控制字段
data.maxConcurrentTasks = form.value.maxConcurrentTasks || 0
} else if (form.value.platform === 'openai-responses') {
// OpenAI-Responses 账户特定数据
data.baseApi = form.value.baseApi
@@ -4738,6 +4793,8 @@ const updateAccount = async () => {
// 额度管理字段
data.dailyQuota = form.value.dailyQuota || 0
data.quotaResetTime = form.value.quotaResetTime || '00:00'
// 并发控制字段
data.maxConcurrentTasks = form.value.maxConcurrentTasks || 0
}
// OpenAI-Responses 特定更新