fix(antigravity): add project fallback for standard tier

This commit is contained in:
wucm667
2026-06-25 16:29:33 +08:00
parent 00d68ff6df
commit 650c50e34b
9 changed files with 389 additions and 9 deletions
@@ -774,6 +774,18 @@
</div>
</div>
<div v-if="form.platform === 'antigravity' && antigravityAccountType === 'oauth'">
<label class="input-label">{{ t('admin.accounts.antigravityProjectIdLabel') }}</label>
<input
v-model="antigravityProjectId"
data-testid="antigravity-project-id-input"
type="text"
class="input font-mono"
:placeholder="t('admin.accounts.antigravityProjectIdPlaceholder')"
/>
<p class="input-hint">{{ t('admin.accounts.antigravityProjectIdHint') }}</p>
</div>
<!-- Upstream config (only for Antigravity upstream type) -->
<div v-if="form.platform === 'antigravity' && antigravityAccountType === 'upstream'" class="space-y-4">
<div>
@@ -3238,7 +3250,10 @@ import ProxyAdBanner from '@/components/common/ProxyAdBanner.vue'
import GroupSelector from '@/components/common/GroupSelector.vue'
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
import QuotaLimitCard from '@/components/account/QuotaLimitCard.vue'
import { applyInterceptWarmup } from '@/components/account/credentialsBuilder'
import {
applyAntigravityProjectID,
applyInterceptWarmup
} from '@/components/account/credentialsBuilder'
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
import { createStableObjectKeyResolver } from '@/utils/stableObjectKey'
import { VERTEX_LOCATION_OPTIONS } from '@/constants/account'
@@ -3440,6 +3455,7 @@ loadQuotaNotifyGlobal()
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
const allowOverages = ref(false) // For antigravity accounts: enable AI Credits overages
const antigravityAccountType = ref<'oauth' | 'upstream'>('oauth') // For antigravity: oauth or upstream
const antigravityProjectId = ref('')
const upstreamBaseUrl = ref('') // For upstream type: base URL
const upstreamApiKey = ref('') // For upstream type: API key
const antigravityModelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
@@ -3813,6 +3829,7 @@ watch(
antigravityAccountType.value = 'oauth'
} else {
allowOverages.value = false
antigravityProjectId.value = ''
antigravityWhitelistModels.value = []
antigravityModelMappings.value = []
antigravityModelRestrictionMode.value = 'mapping'
@@ -4271,6 +4288,7 @@ const resetForm = () => {
customBaseUrl.value = ''
allowOverages.value = false
antigravityAccountType.value = 'oauth'
antigravityProjectId.value = ''
upstreamBaseUrl.value = ''
upstreamApiKey.value = ''
vertexServiceAccountJson.value = ''
@@ -5133,6 +5151,7 @@ const handleAntigravityValidateRT = async (refreshTokenInput: string) => {
}
const credentials = antigravityOAuth.buildCredentials(tokenInfo)
applyAntigravityProjectID(credentials, antigravityProjectId.value, 'create')
// Generate account name with index for batch
const accountName = refreshTokens.length > 1 ? `${form.name} #${i + 1}` : form.name
@@ -5249,6 +5268,7 @@ const handleAntigravityExchange = async (authCode: string) => {
if (!tokenInfo) return
const credentials = antigravityOAuth.buildCredentials(tokenInfo)
applyAntigravityProjectID(credentials, antigravityProjectId.value, 'create')
applyInterceptWarmup(credentials, interceptWarmupRequests.value, 'create')
// Antigravity 只使用映射模式
const antigravityModelMapping = buildModelMappingObject(
@@ -1000,6 +1000,21 @@
</div>
</div>
<div
v-if="account.platform === 'antigravity' && account.type === 'oauth'"
class="border-t border-gray-200 pt-4 dark:border-dark-600"
>
<label class="input-label">{{ t('admin.accounts.antigravityProjectIdLabel') }}</label>
<input
v-model="antigravityProjectId"
data-testid="antigravity-project-id-input"
type="text"
class="input font-mono"
:placeholder="t('admin.accounts.antigravityProjectIdPlaceholder')"
/>
<p class="input-hint">{{ t('admin.accounts.antigravityProjectIdHint') }}</p>
</div>
<!-- Antigravity model restriction (applies to all antigravity types) -->
<!-- Antigravity 只支持模型映射模式,不支持白名单模式 -->
<div v-if="account.platform === 'antigravity'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
@@ -2398,7 +2413,10 @@ import ProxyAdBanner from '@/components/common/ProxyAdBanner.vue'
import GroupSelector from '@/components/common/GroupSelector.vue'
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
import QuotaLimitCard from '@/components/account/QuotaLimitCard.vue'
import { applyInterceptWarmup } from '@/components/account/credentialsBuilder'
import {
applyAntigravityProjectID,
applyInterceptWarmup
} from '@/components/account/credentialsBuilder'
import { formatDateTime, formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
import { createStableObjectKeyResolver } from '@/utils/stableObjectKey'
import { VERTEX_LOCATION_OPTIONS } from '@/constants/account'
@@ -2531,6 +2549,7 @@ const autoPause5hDisabled = ref(false)
const autoPause7dDisabled = ref(false)
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
const allowOverages = ref(false) // For antigravity accounts: enable AI Credits overages
const antigravityProjectId = ref('')
const antigravityModelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
const antigravityWhitelistModels = ref<string[]>([])
const antigravityModelMappings = ref<ModelMapping[]>([])
@@ -2940,6 +2959,12 @@ const syncFormFromAccount = (newAccount: Account | null) => {
editVertexProjectId.value = ''
editVertexClientEmail.value = ''
editVertexLocation.value = 'us-central1'
antigravityProjectId.value =
newAccount.platform === 'antigravity' &&
newAccount.type === 'oauth' &&
typeof credentials?.antigravity_project_id === 'string'
? credentials.antigravity_project_id.trim()
: ''
// Load mixed scheduling setting (only for antigravity accounts)
mixedScheduling.value = false
@@ -3930,6 +3955,9 @@ const handleSubmit = async () => {
const currentCredentials = (updatePayload.credentials as Record<string, unknown>) ||
((props.account.credentials as Record<string, unknown>) || {})
const newCredentials: Record<string, unknown> = { ...currentCredentials }
if (props.account.type === 'oauth') {
applyAntigravityProjectID(newCredentials, antigravityProjectId.value, 'edit')
}
// 移除旧字段
delete newCredentials.model_whitelist
@@ -167,6 +167,31 @@ function buildVertexAccount() {
} as any
}
function buildAntigravityAccount(projectId = 'configured-project') {
return {
id: 3,
name: 'Antigravity OAuth',
notes: '',
platform: 'antigravity',
type: 'oauth',
credentials: {
antigravity_project_id: projectId,
model_mapping: {
'gemini-2.5-flash': 'gemini-2.5-flash'
}
},
extra: {},
proxy_id: null,
concurrency: 1,
priority: 1,
rate_multiplier: 1,
status: 'active',
group_ids: [],
expires_at: null,
auto_pause_on_expired: false
} as any
}
function mountModal(account = buildAccount()) {
return mount(EditAccountModal, {
props: {
@@ -579,4 +604,43 @@ describe('EditAccountModal', () => {
expect(updateAccountMock).not.toHaveBeenCalled()
})
it('loads and submits Antigravity configured project fallback', async () => {
const account = buildAntigravityAccount('configured-project')
updateAccountMock.mockReset()
checkMixedChannelRiskMock.mockReset()
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
updateAccountMock.mockResolvedValue(account)
const wrapper = mountModal(account)
const input = wrapper.get<HTMLInputElement>('[data-testid="antigravity-project-id-input"]')
expect(input.element.value).toBe('configured-project')
await input.setValue(' updated-project ')
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
expect(updateAccountMock).toHaveBeenCalledTimes(1)
expect(updateAccountMock.mock.calls[0]?.[1]?.credentials?.antigravity_project_id).toBe(
'updated-project'
)
})
it('clears Antigravity configured project fallback when input is empty', async () => {
const account = buildAntigravityAccount('configured-project')
updateAccountMock.mockReset()
checkMixedChannelRiskMock.mockReset()
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
updateAccountMock.mockResolvedValue(account)
const wrapper = mountModal(account)
const input = wrapper.get<HTMLInputElement>('[data-testid="antigravity-project-id-input"]')
await input.setValue('')
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
expect(updateAccountMock).toHaveBeenCalledTimes(1)
expect(updateAccountMock.mock.calls[0]?.[1]?.credentials).not.toHaveProperty(
'antigravity_project_id'
)
})
})
@@ -1,5 +1,9 @@
import { describe, it, expect } from 'vitest'
import { applyInterceptWarmup } from '../credentialsBuilder'
import {
ANTIGRAVITY_PROJECT_ID_CREDENTIAL_KEY,
applyAntigravityProjectID,
applyInterceptWarmup
} from '../credentialsBuilder'
describe('applyInterceptWarmup', () => {
it('create + enabled=true: should set intercept_warmup_requests to true', () => {
@@ -44,3 +48,37 @@ describe('applyInterceptWarmup', () => {
expect('intercept_warmup_requests' in creds).toBe(false)
})
})
describe('applyAntigravityProjectID', () => {
it('create + project id: trims and stores configured project fallback', () => {
const creds: Record<string, unknown> = { access_token: 'tok' }
applyAntigravityProjectID(creds, ' configured-project ', 'create')
expect(creds[ANTIGRAVITY_PROJECT_ID_CREDENTIAL_KEY]).toBe('configured-project')
})
it('create + empty project id: should not add the field', () => {
const creds: Record<string, unknown> = { access_token: 'tok' }
applyAntigravityProjectID(creds, ' ', 'create')
expect(ANTIGRAVITY_PROJECT_ID_CREDENTIAL_KEY in creds).toBe(false)
})
it('edit + empty project id: deletes existing fallback', () => {
const creds: Record<string, unknown> = {
access_token: 'tok',
[ANTIGRAVITY_PROJECT_ID_CREDENTIAL_KEY]: 'old-project'
}
applyAntigravityProjectID(creds, '', 'edit')
expect(ANTIGRAVITY_PROJECT_ID_CREDENTIAL_KEY in creds).toBe(false)
})
it('does not affect onboard project_id or other credentials', () => {
const creds: Record<string, unknown> = {
project_id: 'onboard-project',
model_mapping: { 'gemini-*': 'gemini-2.5-flash' }
}
applyAntigravityProjectID(creds, 'configured-project', 'edit')
expect(creds.project_id).toBe('onboard-project')
expect(creds.model_mapping).toEqual({ 'gemini-*': 'gemini-2.5-flash' })
expect(creds[ANTIGRAVITY_PROJECT_ID_CREDENTIAL_KEY]).toBe('configured-project')
})
})
@@ -9,3 +9,18 @@ export function applyInterceptWarmup(
delete credentials.intercept_warmup_requests
}
}
export const ANTIGRAVITY_PROJECT_ID_CREDENTIAL_KEY = 'antigravity_project_id'
export function applyAntigravityProjectID(
credentials: Record<string, unknown>,
projectId: string,
mode: 'create' | 'edit'
): void {
const trimmed = projectId.trim()
if (trimmed) {
credentials[ANTIGRAVITY_PROJECT_ID_CREDENTIAL_KEY] = trimmed
} else if (mode === 'edit') {
delete credentials[ANTIGRAVITY_PROJECT_ID_CREDENTIAL_KEY]
}
}
+4
View File
@@ -3139,6 +3139,10 @@ export default {
upstream: 'Upstream',
upstreamDesc: 'Connect via Base URL + API Key'
},
antigravityProjectIdLabel: 'GCP Project ID (optional)',
antigravityProjectIdPlaceholder: 'your-gcp-project-id',
antigravityProjectIdHint:
'Antigravity standard-tier accounts that do not receive an automatic project_id need a user-owned GCP project.',
status: {
active: 'Active',
inactive: 'Inactive',
+4
View File
@@ -3329,6 +3329,10 @@ export default {
api_key: 'API Key',
cookie: 'Cookie'
},
antigravityProjectIdLabel: 'GCP Project ID(可选)',
antigravityProjectIdPlaceholder: 'your-gcp-project-id',
antigravityProjectIdHint:
'standard-tier 且未自动返回 project_id 的 Antigravity 账号需要填写用户自带 GCP project。',
status: {
active: '正常',
inactive: '停用',