Merge tag 'v0.1.76' into develop

重构速率限制从 scope 级别到 model 级别,新增分组拖拽排序功能,优化 Antigravity 智能重试和错误处理策略。

- 分组拖拽排序:管理后台支持拖拽调整分组顺序
- 账号调度防雷群:相同优先级的账号在调度时随机打乱,防止并发请求集中到同一账号
- 上游账号路由:AccountTypeUpstream 正确路由到 ForwardUpstream 流程
- 客户端断开检测:流式响应时检测客户端断开,继续消费上游响应以确保计费准确
- Antigravity 失败切换延迟:账号切换时增加线性延迟,避免瞬时切换造成雪崩
- Gemini 错误策略集成:统一 Antigravity 错误策略,支持 Gemini 账号自定义错误码

- 重构速率限制:从 scope 级别(claude/gemini_text/gemini_image)改为 model 级别限速,简化账号选择算法
- 重构会话存储:使用扁平缓存替代 Trie 结构,简化 digest session 存储
- 速率限制延迟:使用上游返回的 retryDelay 而非固定 30s 默认值

- 修复 Gemini 原生请求格式解析,确保 session hash 正确生成
- 修复不同用户发送相同消息时 sessionHash 冲突问题
- 修复 thoughtSignature 清理逻辑,现在适用于所有客户端而非仅 CLI
- 修复粘性会话失败切换时的缓存计费豁免问题

# Conflicts:
#	backend/cmd/server/VERSION
#	backend/internal/service/gateway_multiplatform_test.go
#	backend/internal/service/gemini_multiplatform_test.go
#	frontend/src/components/account/AccountStatusIndicator.vue
#	frontend/src/views/admin/ops/components/OpsConcurrencyCard.vue
This commit is contained in:
erio
2026-02-09 12:40:26 +08:00
38 changed files with 965 additions and 20 deletions

View File

@@ -52,6 +52,14 @@
>
<Icon name="refresh" size="md" :class="loading ? 'animate-spin' : ''" />
</button>
<button
@click="openSortModal"
class="btn btn-secondary"
:title="t('admin.groups.sortOrder')"
>
<Icon name="arrowsUpDown" size="md" class="mr-2" />
{{ t('admin.groups.sortOrder') }}
</button>
<button
@click="showCreateModal = true"
class="btn btn-primary"
@@ -1455,6 +1463,92 @@
@confirm="confirmDelete"
@cancel="showDeleteDialog = false"
/>
<!-- Sort Order Modal -->
<BaseDialog
:show="showSortModal"
:title="t('admin.groups.sortOrder')"
width="normal"
@close="closeSortModal"
>
<div class="space-y-4">
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t('admin.groups.sortOrderHint') }}
</p>
<VueDraggable
v-model="sortableGroups"
:animation="200"
class="space-y-2"
>
<div
v-for="group in sortableGroups"
:key="group.id"
class="flex cursor-grab items-center gap-3 rounded-lg border border-gray-200 bg-white p-3 transition-shadow hover:shadow-md active:cursor-grabbing dark:border-dark-600 dark:bg-dark-700"
>
<div class="text-gray-400">
<Icon name="menu" size="md" />
</div>
<div class="flex-1">
<div class="font-medium text-gray-900 dark:text-white">{{ group.name }}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
<span
:class="[
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium',
group.platform === 'anthropic'
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
: group.platform === 'openai'
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
: group.platform === 'antigravity'
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
]"
>
{{ t('admin.groups.platforms.' + group.platform) }}
</span>
</div>
</div>
<div class="text-sm text-gray-400">
#{{ group.id }}
</div>
</div>
</VueDraggable>
</div>
<template #footer>
<div class="flex justify-end gap-3 pt-4">
<button @click="closeSortModal" type="button" class="btn btn-secondary">
{{ t('common.cancel') }}
</button>
<button
@click="saveSortOrder"
:disabled="sortSubmitting"
class="btn btn-primary"
>
<svg
v-if="sortSubmitting"
class="-ml-1 mr-2 h-4 w-4 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{{ sortSubmitting ? t('common.saving') : t('common.save') }}
</button>
</div>
</template>
</BaseDialog>
</AppLayout>
</template>
@@ -1476,6 +1570,7 @@ import EmptyState from '@/components/common/EmptyState.vue'
import Select from '@/components/common/Select.vue'
import PlatformIcon from '@/components/common/PlatformIcon.vue'
import Icon from '@/components/icons/Icon.vue'
import { VueDraggable } from 'vue-draggable-plus'
const { t } = useI18n()
const appStore = useAppStore()
@@ -1640,9 +1735,12 @@ let abortController: AbortController | null = null
const showCreateModal = ref(false)
const showEditModal = ref(false)
const showDeleteDialog = ref(false)
const showSortModal = ref(false)
const submitting = ref(false)
const sortSubmitting = ref(false)
const editingGroup = ref<AdminGroup | null>(null)
const deletingGroup = ref<AdminGroup | null>(null)
const sortableGroups = ref<AdminGroup[]>([])
const createForm = reactive({
name: '',
@@ -2101,6 +2199,46 @@ const handleClickOutside = (event: MouseEvent) => {
}
}
// 打开排序弹窗
const openSortModal = async () => {
try {
// 获取所有分组(不分页)
const allGroups = await adminAPI.groups.getAll()
// 按 sort_order 排序
sortableGroups.value = [...allGroups].sort((a, b) => a.sort_order - b.sort_order)
showSortModal.value = true
} catch (error) {
appStore.showError(t('admin.groups.failedToLoad'))
console.error('Error loading groups for sorting:', error)
}
}
// 关闭排序弹窗
const closeSortModal = () => {
showSortModal.value = false
sortableGroups.value = []
}
// 保存排序
const saveSortOrder = async () => {
sortSubmitting.value = true
try {
const updates = sortableGroups.value.map((g, index) => ({
id: g.id,
sort_order: index * 10
}))
await adminAPI.groups.updateSortOrder(updates)
appStore.showSuccess(t('admin.groups.sortOrderUpdated'))
closeSortModal()
loadGroups()
} catch (error: any) {
appStore.showError(error.response?.data?.detail || t('admin.groups.failedToUpdateSortOrder'))
console.error('Error updating sort order:', error)
} finally {
sortSubmitting.value = false
}
}
onMounted(() => {
loadGroups()
document.addEventListener('click', handleClickOutside)

View File

@@ -121,6 +121,8 @@ const platformRows = computed((): SummaryRow[] => {
total_accounts: totalAccounts,
available_accounts: availableAccounts,
rate_limited_accounts: safeNumber(avail.rate_limit_count),
error_accounts: safeNumber(avail.error_count),
total_concurrency: totalConcurrency,
used_concurrency: usedConcurrency,
@@ -160,6 +162,10 @@ const groupRows = computed((): SummaryRow[] => {
total_accounts: totalAccounts,
available_accounts: availableAccounts,
rate_limited_accounts: safeNumber(avail.rate_limit_count),
<<<<<<< HEAD
=======
>>>>>>> v0.1.76
error_accounts: safeNumber(avail.error_count),
total_concurrency: totalConcurrency,
used_concurrency: usedConcurrency,
@@ -326,6 +332,8 @@ function formatDuration(seconds: number): string {
return `${hours}h`
}
watch(
() => realtimeEnabled.value,
async (enabled) => {