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
@@ -90,6 +90,10 @@ const (
antigravityFallbackSecondsEnv = "GATEWAY_ANTIGRAVITY_FALLBACK_COOLDOWN_SECONDS"
)
const antigravityProjectIDFallbackCredentialKey = "antigravity_project_id"
var errAntigravityProjectIDRequired = errors.New("该 standard-tier Antigravity 账号需配置 project_id")
// AntigravityAccountSwitchError 账号切换信号
// 当账号限流时间超过阈值时,通知上层切换账号
type AntigravityAccountSwitchError struct {
@@ -1029,6 +1033,22 @@ func (s *AntigravityGatewayService) getMappedModel(account *Account, requestedMo
return mapAntigravityModel(account, requestedModel)
}
func resolveAntigravityProjectID(account *Account) (string, error) {
if account == nil {
return "", errAntigravityProjectIDRequired
}
if projectID := strings.TrimSpace(account.GetCredential("project_id")); projectID != "" {
return projectID, nil
}
if projectID := strings.TrimSpace(account.GetCredential(antigravityProjectIDFallbackCredentialKey)); projectID != "" {
return projectID, nil
}
if projectID := strings.TrimSpace(account.GetExtraString(antigravityProjectIDFallbackCredentialKey)); projectID != "" {
return projectID, nil
}
return "", errAntigravityProjectIDRequired
}
// applyThinkingModelSuffix 根据 thinking 配置调整模型名
// 当映射结果是 claude-sonnet-4-5 且请求开启了 thinking 时,改为 claude-sonnet-4-5-thinking
func applyThinkingModelSuffix(mappedModel string, thinkingEnabled bool) string {
@@ -1068,8 +1088,10 @@ func (s *AntigravityGatewayService) TestConnection(ctx context.Context, account
return nil, fmt.Errorf("获取 access_token 失败: %w", err)
}
// 获取 project_id(部分账户类型可能没有)
projectID := strings.TrimSpace(account.GetCredential("project_id"))
projectID, err := resolveAntigravityProjectID(account)
if err != nil {
return nil, err
}
// 模型映射
mappedModel := s.getMappedModel(account, modelID)
@@ -1326,6 +1348,10 @@ func (s *AntigravityGatewayService) wrapV1InternalRequest(projectID, model strin
if err := json.Unmarshal(originalBody, &request); err != nil {
return nil, fmt.Errorf("解析请求体失败: %w", err)
}
projectID = strings.TrimSpace(projectID)
if projectID == "" {
return nil, errAntigravityProjectIDRequired
}
wrapped := map[string]any{
"project": projectID,
@@ -1403,8 +1429,11 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
}
}
// 获取 project_id(部分账户类型可能没有)
projectID := strings.TrimSpace(account.GetCredential("project_id"))
projectID, err := resolveAntigravityProjectID(account)
if err != nil {
_ = s.writeClaudeError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
return nil, err
}
// 代理 URL
proxyURL := ""
@@ -2171,8 +2200,11 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
}
}
// 获取 project_id(部分账户类型可能没有)
projectID := strings.TrimSpace(account.GetCredential("project_id"))
projectID, err := resolveAntigravityProjectID(account)
if err != nil {
_ = s.writeGoogleError(c, http.StatusBadRequest, err.Error())
return nil, err
}
// 代理 URL
proxyURL := ""
@@ -185,6 +185,21 @@ func (s *queuedHTTPUpstreamStub) DoWithTLS(req *http.Request, proxyURL string, a
return s.Do(req, proxyURL, accountID, concurrency)
}
type recordingInternal500CounterCache struct {
incrementCalls []int64
resetCalls []int64
}
func (c *recordingInternal500CounterCache) IncrementInternal500Count(_ context.Context, accountID int64) (int64, error) {
c.incrementCalls = append(c.incrementCalls, accountID)
return int64(len(c.incrementCalls)), nil
}
func (c *recordingInternal500CounterCache) ResetInternal500Count(_ context.Context, accountID int64) error {
c.resetCalls = append(c.resetCalls, accountID)
return nil
}
type antigravitySettingRepoStub struct{}
func (s *antigravitySettingRepoStub) Get(ctx context.Context, key string) (*Setting, error) {
@@ -215,6 +230,157 @@ func (s *antigravitySettingRepoStub) Delete(ctx context.Context, key string) err
panic("unexpected Delete call")
}
func TestResolveAntigravityProjectID(t *testing.T) {
tests := []struct {
name string
account *Account
want string
wantErr bool
}{
{
name: "uses onboard project_id first",
account: &Account{Credentials: map[string]any{
"project_id": " onboard-project ",
antigravityProjectIDFallbackCredentialKey: " configured-project ",
}},
want: "onboard-project",
},
{
name: "uses configured credentials fallback",
account: &Account{Credentials: map[string]any{
antigravityProjectIDFallbackCredentialKey: " configured-project ",
}},
want: "configured-project",
},
{
name: "uses configured extra fallback",
account: &Account{Extra: map[string]any{
antigravityProjectIDFallbackCredentialKey: " extra-project ",
}},
want: "extra-project",
},
{
name: "missing project",
account: &Account{Credentials: map[string]any{}},
wantErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := resolveAntigravityProjectID(tc.account)
if tc.wantErr {
require.ErrorIs(t, err, errAntigravityProjectIDRequired)
require.Empty(t, got)
return
}
require.NoError(t, err)
require.Equal(t, tc.want, got)
})
}
}
func TestAntigravityGatewayService_ForwardGemini_UsesConfiguredProjectFallback(t *testing.T) {
gin.SetMode(gin.TestMode)
writer := httptest.NewRecorder()
c, _ := gin.CreateTestContext(writer)
body, err := json.Marshal(map[string]any{
"contents": []map[string]any{
{"role": "user", "parts": []map[string]any{{"text": "hello"}}},
},
})
require.NoError(t, err)
c.Request = httptest.NewRequest(http.MethodPost, "/antigravity/v1beta/models/gemini-2.5-flash:streamGenerateContent", bytes.NewReader(body))
upstreamBody := []byte("data: {\"response\":{\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"ok\"}]},\"finishReason\":\"STOP\"}],\"usageMetadata\":{\"promptTokenCount\":1,\"candidatesTokenCount\":1}}}\n\n")
upstream := &queuedHTTPUpstreamStub{
responses: []*http.Response{
{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"text/event-stream"}},
Body: io.NopCloser(bytes.NewReader(upstreamBody)),
},
},
}
svc := &AntigravityGatewayService{
settingService: NewSettingService(&antigravitySettingRepoStub{}, &config.Config{Gateway: config.GatewayConfig{MaxLineSize: defaultMaxLineSize}}),
tokenProvider: &AntigravityTokenProvider{},
httpUpstream: upstream,
}
account := &Account{
ID: 101,
Name: "acc-configured-project",
Platform: PlatformAntigravity,
Type: AccountTypeOAuth,
Status: StatusActive,
Concurrency: 1,
Credentials: map[string]any{
"access_token": "token",
antigravityProjectIDFallbackCredentialKey: "configured-project",
"model_mapping": map[string]any{
"gemini-2.5-flash": "gemini-2.5-flash",
},
},
}
result, err := svc.ForwardGemini(context.Background(), c, account, "gemini-2.5-flash", "streamGenerateContent", true, body, false)
require.NoError(t, err)
require.NotNil(t, result)
require.Len(t, upstream.requestBodies, 1)
var wrapped map[string]any
require.NoError(t, json.Unmarshal(upstream.requestBodies[0], &wrapped))
require.Equal(t, "configured-project", wrapped["project"])
}
func TestAntigravityGatewayService_ForwardGemini_MissingProjectReturnsLocalError(t *testing.T) {
gin.SetMode(gin.TestMode)
writer := httptest.NewRecorder()
c, _ := gin.CreateTestContext(writer)
body, err := json.Marshal(map[string]any{
"contents": []map[string]any{
{"role": "user", "parts": []map[string]any{{"text": "hello"}}},
},
})
require.NoError(t, err)
c.Request = httptest.NewRequest(http.MethodPost, "/antigravity/v1beta/models/gemini-2.5-flash:streamGenerateContent", bytes.NewReader(body))
upstream := &queuedHTTPUpstreamStub{}
internal500Cache := &recordingInternal500CounterCache{}
svc := &AntigravityGatewayService{
tokenProvider: &AntigravityTokenProvider{},
httpUpstream: upstream,
internal500Cache: internal500Cache,
}
account := &Account{
ID: 102,
Name: "acc-missing-project",
Platform: PlatformAntigravity,
Type: AccountTypeOAuth,
Status: StatusActive,
Concurrency: 1,
Credentials: map[string]any{
"access_token": "token",
"model_mapping": map[string]any{
"gemini-2.5-flash": "gemini-2.5-flash",
},
},
}
result, err := svc.ForwardGemini(context.Background(), c, account, "gemini-2.5-flash", "streamGenerateContent", true, body, false)
require.Nil(t, result)
require.ErrorIs(t, err, errAntigravityProjectIDRequired)
require.Equal(t, http.StatusBadRequest, writer.Code)
require.Empty(t, upstream.requestBodies)
require.Empty(t, internal500Cache.incrementCalls)
require.Contains(t, writer.Body.String(), "project_id")
require.NotContains(t, writer.Body.String(), `"project":""`)
}
func TestAntigravityGatewayService_Forward_PromptTooLong(t *testing.T) {
gin.SetMode(gin.TestMode)
writer := httptest.NewRecorder()
@@ -255,6 +421,7 @@ func TestAntigravityGatewayService_Forward_PromptTooLong(t *testing.T) {
Concurrency: 1,
Credentials: map[string]any{
"access_token": "token",
"project_id": "proj",
},
}
@@ -313,6 +480,7 @@ func TestAntigravityGatewayService_Forward_ModelRateLimitTriggersFailover(t *tes
Concurrency: 1,
Credentials: map[string]any{
"access_token": "token",
"project_id": "proj",
},
Extra: map[string]any{
modelRateLimitsKey: map[string]any{
@@ -369,6 +537,7 @@ func TestAntigravityGatewayService_ForwardGemini_ModelRateLimitTriggersFailover(
Concurrency: 1,
Credentials: map[string]any{
"access_token": "token",
"project_id": "proj",
},
Extra: map[string]any{
modelRateLimitsKey: map[string]any{
@@ -423,6 +592,7 @@ func TestAntigravityGatewayService_Forward_StickySessionForceCacheBilling(t *tes
Concurrency: 1,
Credentials: map[string]any{
"access_token": "token",
"project_id": "proj",
},
Extra: map[string]any{
modelRateLimitsKey: map[string]any{
@@ -478,6 +648,7 @@ func TestAntigravityGatewayService_ForwardGemini_StickySessionForceCacheBilling(
Concurrency: 1,
Credentials: map[string]any{
"access_token": "token",
"project_id": "proj",
},
Extra: map[string]any{
modelRateLimitsKey: map[string]any{
@@ -623,6 +794,7 @@ func TestAntigravityGatewayService_Forward_BillsWithMappedModel(t *testing.T) {
Concurrency: 1,
Credentials: map[string]any{
"access_token": "token",
"project_id": "proj",
"model_mapping": map[string]any{
"claude-sonnet-4-5": mappedModel,
},
@@ -676,6 +848,7 @@ func TestAntigravityGatewayService_ForwardGemini_BillsWithMappedModel(t *testing
Concurrency: 1,
Credentials: map[string]any{
"access_token": "token",
"project_id": "proj",
"model_mapping": map[string]any{
"gemini-2.5-flash": mappedModel,
},
@@ -747,6 +920,7 @@ func TestAntigravityGatewayService_ForwardGemini_RetriesCorruptedThoughtSignatur
Concurrency: 1,
Credentials: map[string]any{
"access_token": "token",
"project_id": "proj",
"model_mapping": map[string]any{
originalModel: mappedModel,
},
@@ -805,6 +979,7 @@ func TestAntigravityGatewayService_ForwardGemini_SignatureRetryPropagatesFailove
Concurrency: 1,
Credentials: map[string]any{
"access_token": "token",
"project_id": "proj",
"model_mapping": map[string]any{
originalModel: mappedModel,
},
@@ -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: '停用',