feat(antigravity): comprehensive enhancements - model mapping, rate limiting, scheduling & ops
Key changes: - Upgrade model mapping: Opus 4.5 → Opus 4.6-thinking with precise matching - Unified rate limiting: scope-level → model-level with Redis snapshot sync - Load-balanced scheduling by call count with smart retry mechanism - Force cache billing support - Model identity injection in prompts with leak prevention - Thinking mode auto-handling (max_tokens/budget_tokens fix) - Frontend: whitelist mode toggle, model mapping validation, status indicators - Gemini session fallback with Redis Trie O(L) matching - Ops: enhanced concurrency monitoring, account availability, retry logic - Migration scripts: 049-051 for model mapping unification
This commit is contained in:
@@ -2,10 +2,50 @@
|
||||
<AppLayout>
|
||||
<TablePageLayout>
|
||||
<template #filters>
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<!-- Search + Filters -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="relative min-w-0 flex-1 lg:w-48 lg:flex-none">
|
||||
<div class="space-y-3">
|
||||
<!-- Row 1: Actions -->
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
@click="loadProxies"
|
||||
:disabled="loading"
|
||||
class="btn btn-secondary"
|
||||
:title="t('common.refresh')"
|
||||
>
|
||||
<Icon name="refresh" size="md" :class="loading ? 'animate-spin' : ''" />
|
||||
</button>
|
||||
<button
|
||||
@click="handleBatchTest"
|
||||
:disabled="batchTesting || loading"
|
||||
class="btn btn-secondary"
|
||||
:title="t('admin.proxies.testConnection')"
|
||||
>
|
||||
<Icon name="play" size="md" class="mr-2" />
|
||||
{{ t('admin.proxies.testConnection') }}
|
||||
</button>
|
||||
<button
|
||||
@click="openBatchDelete"
|
||||
:disabled="selectedCount === 0"
|
||||
class="btn btn-danger"
|
||||
:title="t('admin.proxies.batchDeleteAction')"
|
||||
>
|
||||
<Icon name="trash" size="md" class="mr-2" />
|
||||
{{ t('admin.proxies.batchDeleteAction') }}
|
||||
</button>
|
||||
<button @click="showImportData = true" class="btn btn-secondary">
|
||||
{{ t('admin.proxies.dataImport') }}
|
||||
</button>
|
||||
<button @click="showExportDataDialog = true" class="btn btn-secondary">
|
||||
{{ selectedCount > 0 ? t('admin.proxies.dataExportSelected') : t('admin.proxies.dataExport') }}
|
||||
</button>
|
||||
<button @click="showCreateModal = true" class="btn btn-primary">
|
||||
<Icon name="plus" size="md" class="mr-2" />
|
||||
{{ t('admin.proxies.createProxy') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: Search + Filters -->
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<div class="relative w-full sm:w-64">
|
||||
<Icon
|
||||
name="search"
|
||||
size="md"
|
||||
@@ -20,7 +60,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-28 lg:w-36">
|
||||
<div class="w-full sm:w-40">
|
||||
<Select
|
||||
v-model="filters.protocol"
|
||||
:options="protocolOptions"
|
||||
@@ -28,7 +68,7 @@
|
||||
@change="loadProxies"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-24 lg:w-32">
|
||||
<div class="w-full sm:w-36">
|
||||
<Select
|
||||
v-model="filters.status"
|
||||
:options="statusOptions"
|
||||
@@ -37,48 +77,6 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
@click="loadProxies"
|
||||
:disabled="loading"
|
||||
class="btn btn-secondary btn-sm lg:btn-md"
|
||||
:title="t('common.refresh')"
|
||||
>
|
||||
<Icon name="refresh" size="md" :class="loading ? 'animate-spin' : ''" />
|
||||
</button>
|
||||
<button
|
||||
@click="handleBatchTest"
|
||||
:disabled="batchTesting || loading"
|
||||
class="btn btn-secondary btn-sm lg:btn-md"
|
||||
:title="t('admin.proxies.testConnection')"
|
||||
>
|
||||
<Icon name="play" size="md" class="lg:mr-2" />
|
||||
<span class="hidden lg:inline">{{ t('admin.proxies.testConnection') }}</span>
|
||||
</button>
|
||||
<button
|
||||
@click="openBatchDelete"
|
||||
:disabled="selectedCount === 0"
|
||||
class="btn btn-danger btn-sm lg:btn-md"
|
||||
:title="t('admin.proxies.batchDeleteAction')"
|
||||
>
|
||||
<Icon name="trash" size="md" class="lg:mr-2" />
|
||||
<span class="hidden lg:inline">{{ t('admin.proxies.batchDeleteAction') }}</span>
|
||||
</button>
|
||||
<button @click="showImportData = true" class="btn btn-secondary btn-sm lg:btn-md">
|
||||
<span class="lg:hidden">{{ t('admin.proxies.dataImportShort') }}</span>
|
||||
<span class="hidden lg:inline">{{ t('admin.proxies.dataImport') }}</span>
|
||||
</button>
|
||||
<button @click="showExportDataDialog = true" class="btn btn-secondary btn-sm lg:btn-md">
|
||||
<span class="lg:hidden">{{ t('admin.proxies.dataExportShort') }}</span>
|
||||
<span class="hidden lg:inline">{{ selectedCount > 0 ? t('admin.proxies.dataExportSelected') : t('admin.proxies.dataExport') }}</span>
|
||||
</button>
|
||||
<button @click="showCreateModal = true" class="btn btn-primary btn-sm lg:btn-md">
|
||||
<Icon name="plus" size="md" class="lg:mr-2" />
|
||||
<span class="hidden lg:inline">{{ t('admin.proxies.createProxy') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { opsAPI, type OpsAccountAvailabilityStatsResponse, type OpsConcurrencyStatsResponse } from '@/api/admin/ops'
|
||||
import { opsAPI, type OpsAccountAvailabilityStatsResponse, type OpsConcurrencyStatsResponse, type OpsUserConcurrencyStatsResponse } from '@/api/admin/ops'
|
||||
|
||||
interface Props {
|
||||
platformFilter?: string
|
||||
@@ -20,6 +20,10 @@ const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const concurrency = ref<OpsConcurrencyStatsResponse | null>(null)
|
||||
const availability = ref<OpsAccountAvailabilityStatsResponse | null>(null)
|
||||
const userConcurrency = ref<OpsUserConcurrencyStatsResponse | null>(null)
|
||||
|
||||
// 用户视图开关
|
||||
const showByUser = ref(false)
|
||||
|
||||
const realtimeEnabled = computed(() => {
|
||||
return (concurrency.value?.enabled ?? true) && (availability.value?.enabled ?? true)
|
||||
@@ -30,7 +34,10 @@ function safeNumber(n: unknown): number {
|
||||
}
|
||||
|
||||
// 计算显示维度
|
||||
const displayDimension = computed<'platform' | 'group' | 'account'>(() => {
|
||||
const displayDimension = computed<'platform' | 'group' | 'account' | 'user'>(() => {
|
||||
if (showByUser.value) {
|
||||
return 'user'
|
||||
}
|
||||
if (typeof props.groupIdFilter === 'number' && props.groupIdFilter > 0) {
|
||||
return 'account'
|
||||
}
|
||||
@@ -81,6 +88,18 @@ interface AccountRow {
|
||||
error_message?: string
|
||||
}
|
||||
|
||||
// 用户行数据
|
||||
interface UserRow {
|
||||
key: string
|
||||
user_id: number
|
||||
user_email: string
|
||||
username: string
|
||||
current_in_use: number
|
||||
max_capacity: number
|
||||
waiting_in_queue: number
|
||||
load_percentage: number
|
||||
}
|
||||
|
||||
// 平台维度汇总
|
||||
const platformRows = computed((): SummaryRow[] => {
|
||||
const concStats = concurrency.value?.platform || {}
|
||||
@@ -205,14 +224,37 @@ const accountRows = computed((): AccountRow[] => {
|
||||
})
|
||||
})
|
||||
|
||||
// 用户维度详细
|
||||
const userRows = computed((): UserRow[] => {
|
||||
const userStats = userConcurrency.value?.user || {}
|
||||
|
||||
return Object.keys(userStats)
|
||||
.map(uid => {
|
||||
const u = userStats[uid] || {}
|
||||
return {
|
||||
key: uid,
|
||||
user_id: safeNumber(u.user_id),
|
||||
user_email: u.user_email || `User ${uid}`,
|
||||
username: u.username || '',
|
||||
current_in_use: safeNumber(u.current_in_use),
|
||||
max_capacity: safeNumber(u.max_capacity),
|
||||
waiting_in_queue: safeNumber(u.waiting_in_queue),
|
||||
load_percentage: safeNumber(u.load_percentage)
|
||||
}
|
||||
})
|
||||
.sort((a, b) => b.current_in_use - a.current_in_use || b.load_percentage - a.load_percentage)
|
||||
})
|
||||
|
||||
// 根据维度选择数据
|
||||
const displayRows = computed(() => {
|
||||
if (displayDimension.value === 'user') return userRows.value
|
||||
if (displayDimension.value === 'account') return accountRows.value
|
||||
if (displayDimension.value === 'group') return groupRows.value
|
||||
return platformRows.value
|
||||
})
|
||||
|
||||
const displayTitle = computed(() => {
|
||||
if (displayDimension.value === 'user') return t('admin.ops.concurrency.byUser')
|
||||
if (displayDimension.value === 'account') return t('admin.ops.concurrency.byAccount')
|
||||
if (displayDimension.value === 'group') return t('admin.ops.concurrency.byGroup')
|
||||
return t('admin.ops.concurrency.byPlatform')
|
||||
@@ -222,12 +264,19 @@ async function loadData() {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const [concData, availData] = await Promise.all([
|
||||
opsAPI.getConcurrencyStats(props.platformFilter, props.groupIdFilter),
|
||||
opsAPI.getAccountAvailabilityStats(props.platformFilter, props.groupIdFilter)
|
||||
])
|
||||
concurrency.value = concData
|
||||
availability.value = availData
|
||||
if (showByUser.value) {
|
||||
// 用户视图模式只加载用户并发数据
|
||||
const userData = await opsAPI.getUserConcurrencyStats()
|
||||
userConcurrency.value = userData
|
||||
} else {
|
||||
// 常规模式加载账号/平台/分组数据
|
||||
const [concData, availData] = await Promise.all([
|
||||
opsAPI.getConcurrencyStats(props.platformFilter, props.groupIdFilter),
|
||||
opsAPI.getAccountAvailabilityStats(props.platformFilter, props.groupIdFilter)
|
||||
])
|
||||
concurrency.value = concData
|
||||
availability.value = availData
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('[OpsConcurrencyCard] Failed to load data', err)
|
||||
errorMessage.value = err?.response?.data?.detail || t('admin.ops.concurrency.loadFailed')
|
||||
@@ -245,6 +294,14 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// 切换用户视图时重新加载数据
|
||||
watch(
|
||||
() => showByUser.value,
|
||||
() => {
|
||||
loadData()
|
||||
}
|
||||
)
|
||||
|
||||
function getLoadBarClass(loadPct: number): string {
|
||||
if (loadPct >= 90) return 'bg-red-500 dark:bg-red-600'
|
||||
if (loadPct >= 70) return 'bg-orange-500 dark:bg-orange-600'
|
||||
@@ -302,16 +359,32 @@ watch(
|
||||
</svg>
|
||||
{{ t('admin.ops.concurrency.title') }}
|
||||
</h3>
|
||||
<button
|
||||
class="flex items-center gap-1 rounded-lg bg-gray-100 px-2 py-1 text-[11px] font-semibold text-gray-700 transition-colors hover:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-dark-700 dark:text-gray-300 dark:hover:bg-dark-600"
|
||||
:disabled="loading"
|
||||
:title="t('common.refresh')"
|
||||
@click="loadData"
|
||||
>
|
||||
<svg class="h-3 w-3" :class="{ 'animate-spin': loading }" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 用户视图切换按钮 -->
|
||||
<button
|
||||
class="flex items-center justify-center rounded-lg px-2 py-1 transition-colors"
|
||||
:class="showByUser
|
||||
? 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
: 'bg-gray-100 text-gray-500 hover:bg-gray-200 hover:text-gray-700 dark:bg-dark-700 dark:text-gray-400 dark:hover:bg-dark-600 dark:hover:text-gray-300'"
|
||||
:title="showByUser ? t('admin.ops.concurrency.switchToPlatform') : t('admin.ops.concurrency.switchToUser')"
|
||||
@click="showByUser = !showByUser"
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- 刷新按钮 -->
|
||||
<button
|
||||
class="flex items-center gap-1 rounded-lg bg-gray-100 px-2 py-1 text-[11px] font-semibold text-gray-700 transition-colors hover:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-dark-700 dark:text-gray-300 dark:hover:bg-dark-600"
|
||||
:disabled="loading"
|
||||
:title="t('common.refresh')"
|
||||
@click="loadData"
|
||||
>
|
||||
<svg class="h-3 w-3" :class="{ 'animate-spin': loading }" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
@@ -344,8 +417,41 @@ watch(
|
||||
{{ t('admin.ops.concurrency.empty') }}
|
||||
</div>
|
||||
|
||||
<!-- 用户视图 -->
|
||||
<div v-else-if="displayDimension === 'user'" class="custom-scrollbar max-h-[360px] flex-1 space-y-2 overflow-y-auto p-3">
|
||||
<div v-for="row in (displayRows as UserRow[])" :key="row.key" class="rounded-lg bg-gray-50 p-2.5 dark:bg-dark-900">
|
||||
<!-- 用户信息和并发 -->
|
||||
<div class="mb-1.5 flex items-center justify-between gap-2">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-1.5">
|
||||
<span class="truncate text-[11px] font-bold text-gray-900 dark:text-white" :title="row.username || row.user_email">
|
||||
{{ row.username || row.user_email }}
|
||||
</span>
|
||||
<span v-if="row.username" class="shrink-0 truncate text-[10px] text-gray-400 dark:text-gray-500" :title="row.user_email">
|
||||
{{ row.user_email }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-2 text-[10px]">
|
||||
<span class="font-mono font-bold text-gray-900 dark:text-white"> {{ row.current_in_use }}/{{ row.max_capacity }} </span>
|
||||
<span :class="['font-bold', getLoadTextClass(row.load_percentage)]"> {{ Math.round(row.load_percentage) }}% </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="h-1.5 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-dark-700">
|
||||
<div class="h-full rounded-full transition-all duration-300" :class="getLoadBarClass(row.load_percentage)" :style="getLoadBarStyle(row.load_percentage)"></div>
|
||||
</div>
|
||||
|
||||
<!-- 等待队列 -->
|
||||
<div v-if="row.waiting_in_queue > 0" class="mt-1.5 flex justify-end">
|
||||
<span class="rounded-full bg-purple-100 px-1.5 py-0.5 text-[10px] font-semibold text-purple-700 dark:bg-purple-900/30 dark:text-purple-400">
|
||||
{{ t('admin.ops.concurrency.queued', { count: row.waiting_in_queue }) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 汇总视图(平台/分组) -->
|
||||
<div v-else-if="displayDimension !== 'account'" class="custom-scrollbar max-h-[360px] flex-1 space-y-2 overflow-y-auto p-3">
|
||||
<div v-else-if="displayDimension === 'platform' || displayDimension === 'group'" class="custom-scrollbar max-h-[360px] flex-1 space-y-2 overflow-y-auto p-3">
|
||||
<div v-for="row in (displayRows as SummaryRow[])" :key="row.key" class="rounded-lg bg-gray-50 p-3 dark:bg-dark-900">
|
||||
<!-- 标题行 -->
|
||||
<div class="mb-2 flex items-center justify-between gap-2">
|
||||
|
||||
Reference in New Issue
Block a user