feat(antigravity): 添加混合调度可选功能

- 后端:账户模型添加 IsMixedSchedulingEnabled() 方法,读取 extra.mixed_scheduling
- 后端:gateway_service 和 gemini_messages_compat_service 支持混合调度逻辑
- 后端:分组创建支持指定 platform 参数
- 前端:账户创建/编辑弹窗添加混合调度开关(仅 antigravity 账户显示)
- 前端:混合调度开关添加问号图标和 tooltip 说明
- 前端:GroupSelector 支持根据 mixedScheduling 属性过滤分组
- 前端:分组创建支持选择 platform
- 测试:e2e 测试添加 ENDPOINT_PREFIX 环境变量支持混合/隔离模式测试
- 测试:删除过时的 Claude signature 测试用例
This commit is contained in:
song
2025-12-29 09:44:39 +08:00
parent cf1d0f23cc
commit 1ad29032d3
15 changed files with 846 additions and 555 deletions

View File

@@ -1025,8 +1025,43 @@
</div>
</div>
<!-- Mixed Scheduling (only for antigravity accounts) -->
<div v-if="form.platform === 'antigravity'" class="flex items-center gap-2">
<label class="flex cursor-pointer items-center gap-2">
<input
type="checkbox"
v-model="mixedScheduling"
class="h-4 w-4 rounded border-gray-300 text-primary-500 focus:ring-primary-500 dark:border-dark-500"
/>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('admin.accounts.mixedScheduling') }}
</span>
</label>
<div class="group relative">
<span
class="inline-flex h-4 w-4 cursor-help items-center justify-center rounded-full bg-gray-200 text-xs text-gray-500 hover:bg-gray-300 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500"
>
?
</span>
<!-- Tooltip向下显示避免被弹窗裁剪 -->
<div
class="pointer-events-none absolute left-0 top-full z-[100] mt-1.5 w-72 rounded bg-gray-900 px-3 py-2 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
>
{{ t('admin.accounts.mixedSchedulingTooltip') }}
<div
class="absolute bottom-full left-3 border-4 border-transparent border-b-gray-900 dark:border-b-gray-700"
></div>
</div>
</div>
</div>
<!-- Group Selection -->
<GroupSelector v-model="form.group_ids" :groups="groups" :platform="form.platform" />
<GroupSelector
v-model="form.group_ids"
:groups="groups"
:platform="form.platform"
:mixed-scheduling="mixedScheduling"
/>
</form>
@@ -1244,6 +1279,7 @@ const customErrorCodesEnabled = ref(false)
const selectedErrorCodes = ref<number[]>([])
const customErrorCodeInput = ref<number | null>(null)
const interceptWarmupRequests = ref(false)
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
const geminiOAuthType = ref<'code_assist' | 'ai_studio'>('code_assist')
const geminiAIStudioOAuthEnabled = ref(false)
@@ -1730,7 +1766,7 @@ const createAccountAndFinish = async (
platform: AccountPlatform,
type: AccountType,
credentials: Record<string, unknown>,
extra?: Record<string, string>
extra?: Record<string, unknown>
) => {
await adminAPI.accounts.create({
name: form.name,
@@ -1834,7 +1870,8 @@ const handleAntigravityExchange = async (authCode: string) => {
if (!tokenInfo) return
const credentials = antigravityOAuth.buildCredentials(tokenInfo)
await createAccountAndFinish('antigravity', 'oauth', credentials)
const extra = mixedScheduling.value ? { mixed_scheduling: true } : undefined
await createAccountAndFinish('antigravity', 'oauth', credentials, extra)
} catch (error: any) {
antigravityOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
appStore.showError(antigravityOAuth.error.value)

View File

@@ -466,8 +466,44 @@
<Select v-model="form.status" :options="statusOptions" />
</div>
<!-- Mixed Scheduling (only for antigravity accounts, read-only in edit mode) -->
<div v-if="account?.platform === 'antigravity'" class="flex items-center gap-2">
<label class="flex cursor-not-allowed items-center gap-2 opacity-60">
<input
type="checkbox"
v-model="mixedScheduling"
disabled
class="h-4 w-4 cursor-not-allowed rounded border-gray-300 text-primary-500 focus:ring-primary-500 dark:border-dark-500"
/>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('admin.accounts.mixedScheduling') }}
</span>
</label>
<div class="group relative">
<span
class="inline-flex h-4 w-4 cursor-help items-center justify-center rounded-full bg-gray-200 text-xs text-gray-500 hover:bg-gray-300 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500"
>
?
</span>
<!-- Tooltip向下显示避免被弹窗裁剪 -->
<div
class="pointer-events-none absolute left-0 top-full z-[100] mt-1.5 w-72 rounded bg-gray-900 px-3 py-2 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
>
{{ t('admin.accounts.mixedSchedulingTooltip') }}
<div
class="absolute bottom-full left-3 border-4 border-transparent border-b-gray-900 dark:border-b-gray-700"
></div>
</div>
</div>
</div>
<!-- Group Selection -->
<GroupSelector v-model="form.group_ids" :groups="groups" :platform="account?.platform" />
<GroupSelector
v-model="form.group_ids"
:groups="groups"
:platform="account?.platform"
:mixed-scheduling="mixedScheduling"
/>
</form>
@@ -553,6 +589,7 @@ const customErrorCodesEnabled = ref(false)
const selectedErrorCodes = ref<number[]>([])
const customErrorCodeInput = ref<number | null>(null)
const interceptWarmupRequests = ref(false)
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
// Common models for whitelist - Anthropic
const anthropicModels = [
@@ -764,6 +801,10 @@ watch(
const credentials = newAccount.credentials as Record<string, unknown> | undefined
interceptWarmupRequests.value = credentials?.intercept_warmup_requests === true
// Load mixed scheduling setting (only for antigravity accounts)
const extra = newAccount.extra as Record<string, unknown> | undefined
mixedScheduling.value = extra?.mixed_scheduling === true
// Initialize API Key fields for apikey type
if (newAccount.type === 'apikey' && newAccount.credentials) {
const credentials = newAccount.credentials as Record<string, unknown>
@@ -969,6 +1010,18 @@ const handleSubmit = async () => {
updatePayload.credentials = newCredentials
}
// For antigravity accounts, handle mixed_scheduling in extra
if (props.account.platform === 'antigravity') {
const currentExtra = (props.account.extra as Record<string, unknown>) || {}
const newExtra: Record<string, unknown> = { ...currentExtra }
if (mixedScheduling.value) {
newExtra.mixed_scheduling = true
} else {
delete newExtra.mixed_scheduling
}
updatePayload.extra = newExtra
}
await adminAPI.accounts.update(props.account.id, updatePayload)
appStore.showSuccess(t('admin.accounts.accountUpdated'))
emit('updated')

View File

@@ -50,6 +50,7 @@ interface Props {
modelValue: number[]
groups: Group[]
platform?: GroupPlatform // Optional platform filter
mixedScheduling?: boolean // For antigravity accounts: allow anthropic/gemini groups
}
const props = defineProps<Props>()
@@ -62,10 +63,13 @@ const filteredGroups = computed(() => {
if (!props.platform) {
return props.groups
}
// antigravity 账户可选择 anthropicgemini 平台的分组
if (props.platform === 'antigravity') {
return props.groups.filter((g) => g.platform === 'anthropic' || g.platform === 'gemini')
// antigravity 账户启用混合调度后,可选择 anthropic/gemini 分组
if (props.platform === 'antigravity' && props.mixedScheduling) {
return props.groups.filter(
(g) => g.platform === 'antigravity' || g.platform === 'anthropic' || g.platform === 'gemini'
)
}
// 默认:只能选择同 platform 的分组
return props.groups.filter((g) => g.platform === props.platform)
})