Merge PR #73: feat(antigravity): 添加 Antigravity (Cloud AI Companion) 平台支持

新增功能:
- Antigravity OAuth 授权流程支持
- Claude → Gemini 协议转换(Claude API 请求自动转换为 Gemini 格式)
- 配额刷新和状态显示
- 混合调度功能,支持 Anthropic 和 Antigravity 账户混合使用
- /antigravity 专用路由,支持仅使用 Antigravity 账户
- 前端 Antigravity 服务商标识和账户管理功能

冲突解决:
- CreateAccountModal.vue: 合并 data-tour 属性和 mixed-scheduling 属性
- EditAccountModal.vue: 合并 data-tour 属性和 mixed-scheduling 属性

代码质量改进:
- 修复 antigravity 类型文件的 gofmt 格式问题(struct 字段对齐、interface{} → any)
- 移除 .golangci.yml 中的 gofmt 排除规则
- 修复测试文件的格式问题
This commit is contained in:
shaw
2025-12-29 20:32:20 +08:00
61 changed files with 7760 additions and 260 deletions

View File

@@ -0,0 +1,56 @@
/**
* Admin Antigravity API endpoints
* Handles Antigravity (Google Cloud AI Companion) OAuth flows for administrators
*/
import { apiClient } from '../client'
export interface AntigravityAuthUrlResponse {
auth_url: string
session_id: string
state: string
}
export interface AntigravityAuthUrlRequest {
proxy_id?: number
}
export interface AntigravityExchangeCodeRequest {
session_id: string
state: string
code: string
proxy_id?: number
}
export interface AntigravityTokenInfo {
access_token?: string
refresh_token?: string
token_type?: string
expires_at?: number | string
expires_in?: number
project_id?: string
email?: string
[key: string]: unknown
}
export async function generateAuthUrl(
payload: AntigravityAuthUrlRequest
): Promise<AntigravityAuthUrlResponse> {
const { data } = await apiClient.post<AntigravityAuthUrlResponse>(
'/admin/antigravity/oauth/auth-url',
payload
)
return data
}
export async function exchangeCode(
payload: AntigravityExchangeCodeRequest
): Promise<AntigravityTokenInfo> {
const { data } = await apiClient.post<AntigravityTokenInfo>(
'/admin/antigravity/oauth/exchange-code',
payload
)
return data
}
export default { generateAuthUrl, exchangeCode }

View File

@@ -14,6 +14,7 @@ import systemAPI from './system'
import subscriptionsAPI from './subscriptions'
import usageAPI from './usage'
import geminiAPI from './gemini'
import antigravityAPI from './antigravity'
/**
* Unified admin API object for convenient access
@@ -29,7 +30,8 @@ export const adminAPI = {
system: systemAPI,
subscriptions: subscriptionsAPI,
usage: usageAPI,
gemini: geminiAPI
gemini: geminiAPI,
antigravity: antigravityAPI
}
export {
@@ -43,7 +45,8 @@ export {
systemAPI,
subscriptionsAPI,
usageAPI,
geminiAPI
geminiAPI,
antigravityAPI
}
export default adminAPI

View File

@@ -93,6 +93,60 @@
<div v-else class="text-xs text-gray-400">-</div>
</template>
<!-- Antigravity OAuth accounts: show quota from extra field -->
<template v-else-if="account.platform === 'antigravity' && account.type === 'oauth'">
<!-- 账户类型徽章 -->
<div v-if="antigravityTierLabel" class="mb-1">
<span
:class="[
'inline-block rounded px-1.5 py-0.5 text-[10px] font-medium',
antigravityTierClass
]"
>
{{ antigravityTierLabel }}
</span>
</div>
<div v-if="hasAntigravityQuota" class="space-y-1">
<!-- Gemini 3 Pro -->
<UsageProgressBar
v-if="antigravity3ProUsage !== null"
:label="t('admin.accounts.usageWindow.gemini3Pro')"
:utilization="antigravity3ProUsage.utilization"
:resets-at="antigravity3ProUsage.resetTime"
color="indigo"
/>
<!-- Gemini 3 Flash -->
<UsageProgressBar
v-if="antigravity3FlashUsage !== null"
:label="t('admin.accounts.usageWindow.gemini3Flash')"
:utilization="antigravity3FlashUsage.utilization"
:resets-at="antigravity3FlashUsage.resetTime"
color="emerald"
/>
<!-- Gemini 3 Image -->
<UsageProgressBar
v-if="antigravity3ImageUsage !== null"
:label="t('admin.accounts.usageWindow.gemini3Image')"
:utilization="antigravity3ImageUsage.utilization"
:resets-at="antigravity3ImageUsage.resetTime"
color="purple"
/>
<!-- Claude 4.5 -->
<UsageProgressBar
v-if="antigravityClaude45Usage !== null"
:label="t('admin.accounts.usageWindow.claude45')"
:utilization="antigravityClaude45Usage.utilization"
:resets-at="antigravityClaude45Usage.resetTime"
color="amber"
/>
</div>
<div v-else class="text-xs text-gray-400">-</div>
</template>
<!-- Other accounts: no usage window -->
<template v-else>
<div class="text-xs text-gray-400">-</div>
@@ -273,6 +327,117 @@ const codex7dResetAt = computed(() => {
return null
})
// Antigravity quota types
interface AntigravityModelQuota {
remaining: number // 剩余百分比 0-100
reset_time: string // ISO 8601 重置时间
}
interface AntigravityQuotaData {
[model: string]: AntigravityModelQuota
}
interface AntigravityUsageResult {
utilization: number
resetTime: string | null
}
// Antigravity quota computed properties
const hasAntigravityQuota = computed(() => {
const extra = props.account.extra as Record<string, unknown> | undefined
return extra && typeof extra.quota === 'object' && extra.quota !== null
})
// 从配额数据中获取使用率(多模型取最低剩余 = 最高使用)
const getAntigravityUsage = (
modelNames: string[]
): AntigravityUsageResult | null => {
const extra = props.account.extra as Record<string, unknown> | undefined
if (!extra || typeof extra.quota !== 'object' || extra.quota === null) return null
const quota = extra.quota as AntigravityQuotaData
let minRemaining = 100
let earliestReset: string | null = null
for (const model of modelNames) {
const modelQuota = quota[model]
if (!modelQuota) continue
if (modelQuota.remaining < minRemaining) {
minRemaining = modelQuota.remaining
}
if (modelQuota.reset_time) {
if (!earliestReset || modelQuota.reset_time < earliestReset) {
earliestReset = modelQuota.reset_time
}
}
}
// 如果没有找到任何匹配的模型
if (minRemaining === 100 && earliestReset === null) {
// 检查是否至少有一个模型有数据
const hasAnyData = modelNames.some((m) => quota[m])
if (!hasAnyData) return null
}
return {
utilization: 100 - minRemaining,
resetTime: earliestReset
}
}
// Gemini 3 Pro: gemini-3-pro-low, gemini-3-pro-high, gemini-3-pro-preview
const antigravity3ProUsage = computed(() =>
getAntigravityUsage(['gemini-3-pro-low', 'gemini-3-pro-high', 'gemini-3-pro-preview'])
)
// Gemini 3 Flash: gemini-3-flash
const antigravity3FlashUsage = computed(() => getAntigravityUsage(['gemini-3-flash']))
// Gemini 3 Image: gemini-3-pro-image
const antigravity3ImageUsage = computed(() => getAntigravityUsage(['gemini-3-pro-image']))
// Claude 4.5: claude-sonnet-4-5, claude-opus-4-5-thinking
const antigravityClaude45Usage = computed(() =>
getAntigravityUsage(['claude-sonnet-4-5', 'claude-opus-4-5-thinking'])
)
// Antigravity 账户类型
const antigravityTier = computed(() => {
const extra = props.account.extra as Record<string, unknown> | undefined
if (!extra || typeof extra.tier !== 'string') return null
return extra.tier as string
})
// 账户类型显示标签
const antigravityTierLabel = computed(() => {
switch (antigravityTier.value) {
case 'free-tier':
return t('admin.accounts.tier.free')
case 'g1-pro-tier':
return t('admin.accounts.tier.pro')
case 'g1-ultra-tier':
return t('admin.accounts.tier.ultra')
default:
return null
}
})
// 账户类型徽章样式
const antigravityTierClass = computed(() => {
switch (antigravityTier.value) {
case 'free-tier':
return 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300'
case 'g1-pro-tier':
return 'bg-blue-100 text-blue-600 dark:bg-blue-900/40 dark:text-blue-300'
case 'g1-ultra-tier':
return 'bg-purple-100 text-purple-600 dark:bg-purple-900/40 dark:text-purple-300'
default:
return ''
}
})
const loadUsage = async () => {
// Fetch usage for Anthropic OAuth and Setup Token accounts
// OpenAI usage comes from account.extra field (updated during forwarding)

View File

@@ -136,6 +136,31 @@
</svg>
Gemini
</button>
<button
type="button"
@click="form.platform = 'antigravity'"
:class="[
'flex flex-1 items-center justify-center gap-2 rounded-md px-4 py-2.5 text-sm font-medium transition-all',
form.platform === 'antigravity'
? 'bg-white text-purple-600 shadow-sm dark:bg-dark-600 dark:text-purple-400'
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
]"
>
<svg
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z"
/>
</svg>
Antigravity
</button>
</div>
</div>
@@ -488,6 +513,36 @@
</div>
</div>
<!-- Account Type Selection (Antigravity - OAuth only) -->
<div v-if="form.platform === 'antigravity'">
<label class="input-label">{{ t('admin.accounts.accountType') }}</label>
<div class="mt-2">
<div
class="flex items-center gap-3 rounded-lg border-2 border-purple-500 bg-purple-50 p-3 dark:bg-purple-900/20"
>
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-purple-500 text-white">
<svg
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
/>
</svg>
</div>
<div>
<span class="block text-sm font-medium text-gray-900 dark:text-white">OAuth</span>
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.types.antigravityOauth') }}</span>
</div>
</div>
</div>
</div>
<!-- Add Method (only for Anthropic OAuth-based type) -->
<div v-if="form.platform === 'anthropic' && isOAuthFlow">
<label class="input-label">{{ t('admin.accounts.addMethod') }}</label>
@@ -971,11 +1026,46 @@
</div>
</div>
<!-- Group Selection - 仅标准模式显示 -->
<div v-if="!authStore.isSimpleMode" data-tour="account-form-groups">
<GroupSelector v-model="form.group_ids" :groups="groups" :platform="form.platform" />
<!-- 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-if="!authStore.isSimpleMode"
v-model="form.group_ids"
:groups="groups"
:platform="form.platform"
:mixed-scheduling="mixedScheduling"
data-tour="account-form-groups"
/>
</form>
<!-- Step 2: OAuth Authorization -->
@@ -1095,6 +1185,7 @@ import {
} from '@/composables/useAccountOAuth'
import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth'
import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth'
import type { Proxy, Group, AccountPlatform, AccountType } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue'
import ProxySelector from '@/components/common/ProxySelector.vue'
@@ -1118,6 +1209,7 @@ const authStore = useAuthStore()
const oauthStepTitle = computed(() => {
if (form.platform === 'openai') return t('admin.accounts.oauth.openai.title')
if (form.platform === 'gemini') return t('admin.accounts.oauth.gemini.title')
if (form.platform === 'antigravity') return t('admin.accounts.oauth.antigravity.title')
return t('admin.accounts.oauth.title')
})
@@ -1152,29 +1244,34 @@ const appStore = useAppStore()
const oauth = useAccountOAuth() // For Anthropic OAuth
const openaiOAuth = useOpenAIOAuth() // For OpenAI OAuth
const geminiOAuth = useGeminiOAuth() // For Gemini OAuth
const antigravityOAuth = useAntigravityOAuth() // For Antigravity OAuth
// Computed: current OAuth state for template binding
const currentAuthUrl = computed(() => {
if (form.platform === 'openai') return openaiOAuth.authUrl.value
if (form.platform === 'gemini') return geminiOAuth.authUrl.value
if (form.platform === 'antigravity') return antigravityOAuth.authUrl.value
return oauth.authUrl.value
})
const currentSessionId = computed(() => {
if (form.platform === 'openai') return openaiOAuth.sessionId.value
if (form.platform === 'gemini') return geminiOAuth.sessionId.value
if (form.platform === 'antigravity') return antigravityOAuth.sessionId.value
return oauth.sessionId.value
})
const currentOAuthLoading = computed(() => {
if (form.platform === 'openai') return openaiOAuth.loading.value
if (form.platform === 'gemini') return geminiOAuth.loading.value
if (form.platform === 'antigravity') return antigravityOAuth.loading.value
return oauth.loading.value
})
const currentOAuthError = computed(() => {
if (form.platform === 'openai') return openaiOAuth.error.value
if (form.platform === 'gemini') return geminiOAuth.error.value
if (form.platform === 'antigravity') return antigravityOAuth.error.value
return oauth.error.value
})
@@ -1201,6 +1298,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)
@@ -1403,6 +1501,9 @@ const canExchangeCode = computed(() => {
if (form.platform === 'gemini') {
return authCode.trim() && geminiOAuth.sessionId.value && !geminiOAuth.loading.value
}
if (form.platform === 'antigravity') {
return authCode.trim() && antigravityOAuth.sessionId.value && !antigravityOAuth.loading.value
}
return authCode.trim() && oauth.sessionId.value && !oauth.loading.value
})
@@ -1447,10 +1548,15 @@ watch(
if (newPlatform !== 'anthropic') {
interceptWarmupRequests.value = false
}
// Antigravity only supports OAuth
if (newPlatform === 'antigravity') {
accountCategory.value = 'oauth-based'
}
// Reset OAuth states
oauth.resetState()
openaiOAuth.resetState()
geminiOAuth.resetState()
antigravityOAuth.resetState()
}
)
@@ -1579,6 +1685,7 @@ const resetForm = () => {
oauth.resetState()
openaiOAuth.resetState()
geminiOAuth.resetState()
antigravityOAuth.resetState()
oauthFlowRef.value?.reset()
}
@@ -1657,6 +1764,7 @@ const goBackToBasicInfo = () => {
oauth.resetState()
openaiOAuth.resetState()
geminiOAuth.resetState()
antigravityOAuth.resetState()
oauthFlowRef.value?.reset()
}
@@ -1665,114 +1773,134 @@ const handleGenerateUrl = async () => {
await openaiOAuth.generateAuthUrl(form.proxy_id)
} else if (form.platform === 'gemini') {
await geminiOAuth.generateAuthUrl(form.proxy_id, oauthFlowRef.value?.projectId, geminiOAuthType.value)
} else if (form.platform === 'antigravity') {
await antigravityOAuth.generateAuthUrl(form.proxy_id)
} else {
await oauth.generateAuthUrl(addMethod.value, form.proxy_id)
}
}
const handleExchangeCode = async () => {
const authCode = oauthFlowRef.value?.authCode || ''
// Create account and handle success/failure
const createAccountAndFinish = async (
platform: AccountPlatform,
type: AccountType,
credentials: Record<string, unknown>,
extra?: Record<string, unknown>
) => {
await adminAPI.accounts.create({
name: form.name,
platform,
type,
credentials,
extra,
proxy_id: form.proxy_id,
concurrency: form.concurrency,
priority: form.priority,
group_ids: form.group_ids
})
appStore.showSuccess(t('admin.accounts.accountCreated'))
emit('created')
handleClose()
}
// For OpenAI
if (form.platform === 'openai') {
if (!authCode.trim() || !openaiOAuth.sessionId.value) return
// OpenAI OAuth 授权码兑换
const handleOpenAIExchange = async (authCode: string) => {
if (!authCode.trim() || !openaiOAuth.sessionId.value) return
openaiOAuth.loading.value = true
openaiOAuth.error.value = ''
openaiOAuth.loading.value = true
openaiOAuth.error.value = ''
try {
const tokenInfo = await openaiOAuth.exchangeAuthCode(
authCode.trim(),
openaiOAuth.sessionId.value,
form.proxy_id
)
try {
const tokenInfo = await openaiOAuth.exchangeAuthCode(
authCode.trim(),
openaiOAuth.sessionId.value,
form.proxy_id
)
if (!tokenInfo) return
if (!tokenInfo) {
return // Error already handled by composable
}
const credentials = openaiOAuth.buildCredentials(tokenInfo)
const extra = openaiOAuth.buildExtraInfo(tokenInfo)
// Note: intercept_warmup_requests is Anthropic-only, not applicable to OpenAI
await adminAPI.accounts.create({
name: form.name,
platform: 'openai',
type: 'oauth',
credentials,
extra,
proxy_id: form.proxy_id,
concurrency: form.concurrency,
priority: form.priority,
group_ids: form.group_ids
})
appStore.showSuccess(t('admin.accounts.accountCreated'))
emit('created')
handleClose()
} catch (error: any) {
openaiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
appStore.showError(openaiOAuth.error.value)
} finally {
openaiOAuth.loading.value = false
}
return
const credentials = openaiOAuth.buildCredentials(tokenInfo)
const extra = openaiOAuth.buildExtraInfo(tokenInfo)
await createAccountAndFinish('openai', 'oauth', credentials, extra)
} catch (error: any) {
openaiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
appStore.showError(openaiOAuth.error.value)
} finally {
openaiOAuth.loading.value = false
}
}
// For Gemini
if (form.platform === 'gemini') {
if (!authCode.trim() || !geminiOAuth.sessionId.value) return
// Gemini OAuth 授权码兑换
const handleGeminiExchange = async (authCode: string) => {
if (!authCode.trim() || !geminiOAuth.sessionId.value) return
geminiOAuth.loading.value = true
geminiOAuth.error.value = ''
geminiOAuth.loading.value = true
geminiOAuth.error.value = ''
try {
const stateFromInput = oauthFlowRef.value?.oauthState || ''
const stateToUse = stateFromInput || geminiOAuth.state.value
if (!stateToUse) {
geminiOAuth.error.value = t('admin.accounts.oauth.authFailed')
appStore.showError(geminiOAuth.error.value)
return
}
const tokenInfo = await geminiOAuth.exchangeAuthCode({
code: authCode.trim(),
sessionId: geminiOAuth.sessionId.value,
state: stateToUse,
proxyId: form.proxy_id,
oauthType: geminiOAuthType.value
})
if (!tokenInfo) return
const credentials = geminiOAuth.buildCredentials(tokenInfo)
// Note: intercept_warmup_requests is Anthropic-only, not applicable to Gemini
await adminAPI.accounts.create({
name: form.name,
platform: 'gemini',
type: 'oauth',
credentials,
proxy_id: form.proxy_id,
concurrency: form.concurrency,
priority: form.priority,
group_ids: form.group_ids
})
appStore.showSuccess(t('admin.accounts.accountCreated'))
emit('created')
handleClose()
} catch (error: any) {
geminiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
try {
const stateFromInput = oauthFlowRef.value?.oauthState || ''
const stateToUse = stateFromInput || geminiOAuth.state.value
if (!stateToUse) {
geminiOAuth.error.value = t('admin.accounts.oauth.authFailed')
appStore.showError(geminiOAuth.error.value)
} finally {
geminiOAuth.loading.value = false
return
}
return
}
// For Anthropic
const tokenInfo = await geminiOAuth.exchangeAuthCode({
code: authCode.trim(),
sessionId: geminiOAuth.sessionId.value,
state: stateToUse,
proxyId: form.proxy_id,
oauthType: geminiOAuthType.value
})
if (!tokenInfo) return
const credentials = geminiOAuth.buildCredentials(tokenInfo)
await createAccountAndFinish('gemini', 'oauth', credentials)
} catch (error: any) {
geminiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
appStore.showError(geminiOAuth.error.value)
} finally {
geminiOAuth.loading.value = false
}
}
// Antigravity OAuth 授权码兑换
const handleAntigravityExchange = async (authCode: string) => {
if (!authCode.trim() || !antigravityOAuth.sessionId.value) return
antigravityOAuth.loading.value = true
antigravityOAuth.error.value = ''
try {
const stateFromInput = oauthFlowRef.value?.oauthState || ''
const stateToUse = stateFromInput || antigravityOAuth.state.value
if (!stateToUse) {
antigravityOAuth.error.value = t('admin.accounts.oauth.authFailed')
appStore.showError(antigravityOAuth.error.value)
return
}
const tokenInfo = await antigravityOAuth.exchangeAuthCode({
code: authCode.trim(),
sessionId: antigravityOAuth.sessionId.value,
state: stateToUse,
proxyId: form.proxy_id
})
if (!tokenInfo) return
const credentials = antigravityOAuth.buildCredentials(tokenInfo)
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)
} finally {
antigravityOAuth.loading.value = false
}
}
// Anthropic OAuth 授权码兑换
const handleAnthropicExchange = async (authCode: string) => {
if (!authCode.trim() || !oauth.sessionId.value) return
oauth.loading.value = true
@@ -1792,28 +1920,11 @@ const handleExchangeCode = async () => {
})
const extra = oauth.buildExtraInfo(tokenInfo)
// Merge interceptWarmupRequests into credentials
const credentials = {
...tokenInfo,
...(interceptWarmupRequests.value ? { intercept_warmup_requests: true } : {})
}
await adminAPI.accounts.create({
name: form.name,
platform: form.platform,
type: addMethod.value, // Use addMethod as type: 'oauth' or 'setup-token'
credentials,
extra,
proxy_id: form.proxy_id,
concurrency: form.concurrency,
priority: form.priority,
group_ids: form.group_ids
})
appStore.showSuccess(t('admin.accounts.accountCreated'))
emit('created')
handleClose()
await createAccountAndFinish(form.platform, addMethod.value as AccountType, credentials, extra)
} catch (error: any) {
oauth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
appStore.showError(oauth.error.value)
@@ -1822,6 +1933,22 @@ const handleExchangeCode = async () => {
}
}
// 主入口:根据平台路由到对应处理函数
const handleExchangeCode = async () => {
const authCode = oauthFlowRef.value?.authCode || ''
switch (form.platform) {
case 'openai':
return handleOpenAIExchange(authCode)
case 'gemini':
return handleGeminiExchange(authCode)
case 'antigravity':
return handleAntigravityExchange(authCode)
default:
return handleAnthropicExchange(authCode)
}
}
const handleCookieAuth = async (sessionKey: string) => {
oauth.loading.value = true
oauth.error.value = ''

View File

@@ -472,11 +472,47 @@
<Select v-model="form.status" :options="statusOptions" />
</div>
<!-- Group Selection - 仅标准模式显示 -->
<div v-if="!authStore.isSimpleMode" data-tour="account-form-groups">
<GroupSelector v-model="form.group_ids" :groups="groups" :platform="account?.platform" />
<!-- 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-if="!authStore.isSimpleMode"
v-model="form.group_ids"
:groups="groups"
:platform="account?.platform"
:mixed-scheduling="mixedScheduling"
data-tour="account-form-groups"
/>
</form>
<template #footer>
@@ -572,6 +608,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 = [
@@ -783,6 +820,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>
@@ -988,6 +1029,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

@@ -527,7 +527,7 @@ interface Props {
allowMultiple?: boolean
methodLabel?: string
showCookieOption?: boolean // Whether to show cookie auto-auth option
platform?: 'anthropic' | 'openai' | 'gemini' // Platform type for different UI/text
platform?: 'anthropic' | 'openai' | 'gemini' | 'antigravity' // Platform type for different UI/text
showProjectId?: boolean // New prop to control project ID visibility
}
@@ -560,6 +560,7 @@ const isOpenAI = computed(() => props.platform === 'openai')
const getOAuthKey = (key: string) => {
if (props.platform === 'openai') return `admin.accounts.oauth.openai.${key}`
if (props.platform === 'gemini') return `admin.accounts.oauth.gemini.${key}`
if (props.platform === 'antigravity') return `admin.accounts.oauth.antigravity.${key}`
return `admin.accounts.oauth.${key}`
}
@@ -575,9 +576,11 @@ const oauthAuthCodeDesc = computed(() => t(getOAuthKey('authCodeDesc')))
const oauthAuthCode = computed(() => t(getOAuthKey('authCode')))
const oauthAuthCodePlaceholder = computed(() => t(getOAuthKey('authCodePlaceholder')))
const oauthAuthCodeHint = computed(() => t(getOAuthKey('authCodeHint')))
const oauthImportantNotice = computed(() =>
props.platform === 'openai' ? t('admin.accounts.oauth.openai.importantNotice') : ''
)
const oauthImportantNotice = computed(() => {
if (props.platform === 'openai') return t('admin.accounts.oauth.openai.importantNotice')
if (props.platform === 'antigravity') return t('admin.accounts.oauth.antigravity.importantNotice')
return ''
})
// Local state
const inputMethod = ref<AuthInputMethod>(props.showCookieOption ? 'manual' : 'manual')
@@ -603,10 +606,10 @@ watch(inputMethod, (newVal) => {
emit('update:inputMethod', newVal)
})
// Auto-extract code from OpenAI callback URL
// e.g., http://localhost:1455/auth/callback?code=ac_xxx...&scope=...&state=...
// Auto-extract code from callback URL (OpenAI/Gemini/Antigravity)
// e.g., http://localhost:8085/callback?code=xxx...&state=...
watch(authCodeInput, (newVal) => {
if (props.platform !== 'openai' && props.platform !== 'gemini') return
if (props.platform !== 'openai' && props.platform !== 'gemini' && props.platform !== 'antigravity') return
const trimmed = newVal.trim()
// Check if it looks like a URL with code parameter
@@ -616,7 +619,7 @@ watch(authCodeInput, (newVal) => {
const url = new URL(trimmed)
const code = url.searchParams.get('code')
const stateParam = url.searchParams.get('state')
if (props.platform === 'gemini' && stateParam) {
if ((props.platform === 'gemini' || props.platform === 'antigravity') && stateParam) {
oauthState.value = stateParam
}
if (code && code !== trimmed) {
@@ -627,7 +630,7 @@ watch(authCodeInput, (newVal) => {
// If URL parsing fails, try regex extraction
const match = trimmed.match(/[?&]code=([^&]+)/)
const stateMatch = trimmed.match(/[?&]state=([^&]+)/)
if (props.platform === 'gemini' && stateMatch && stateMatch[1]) {
if ((props.platform === 'gemini' || props.platform === 'antigravity') && stateMatch && stateMatch[1]) {
oauthState.value = stateMatch[1]
}
if (match && match[1] && match[1] !== trimmed) {

View File

@@ -18,7 +18,9 @@
? 'from-green-500 to-green-600'
: isGemini
? 'from-blue-500 to-blue-600'
: 'from-orange-500 to-orange-600'
: isAntigravity
? 'from-purple-500 to-purple-600'
: 'from-orange-500 to-orange-600'
]"
>
<svg
@@ -45,7 +47,9 @@
? t('admin.accounts.openaiAccount')
: isGemini
? t('admin.accounts.geminiAccount')
: t('admin.accounts.claudeCodeAccount')
: isAntigravity
? t('admin.accounts.antigravityAccount')
: t('admin.accounts.claudeCodeAccount')
}}
</span>
</div>
@@ -201,7 +205,7 @@
:show-cookie-option="isAnthropic"
:allow-multiple="false"
:method-label="t('admin.accounts.inputMethod')"
:platform="isOpenAI ? 'openai' : isGemini ? 'gemini' : 'anthropic'"
:platform="isOpenAI ? 'openai' : isGemini ? 'gemini' : isAntigravity ? 'antigravity' : 'anthropic'"
:show-project-id="isGemini && geminiOAuthType === 'code_assist'"
@generate-url="handleGenerateUrl"
@cookie-auth="handleCookieAuth"
@@ -264,6 +268,7 @@ import {
} from '@/composables/useAccountOAuth'
import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth'
import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth'
import type { Account } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue'
import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue'
@@ -293,10 +298,11 @@ const emit = defineEmits<{
const appStore = useAppStore()
const { t } = useI18n()
// OAuth composables - use both Claude and OpenAI
// OAuth composables
const claudeOAuth = useAccountOAuth()
const openaiOAuth = useOpenAIOAuth()
const geminiOAuth = useGeminiOAuth()
const antigravityOAuth = useAntigravityOAuth()
// Refs
const oauthFlowRef = ref<OAuthFlowExposed | null>(null)
@@ -306,51 +312,48 @@ const addMethod = ref<AddMethod>('oauth')
const geminiOAuthType = ref<'code_assist' | 'ai_studio'>('code_assist')
const geminiAIStudioOAuthEnabled = ref(false)
// Computed - check if this is an OpenAI account
// Computed - check platform
const isOpenAI = computed(() => props.account?.platform === 'openai')
const isGemini = computed(() => props.account?.platform === 'gemini')
const isAnthropic = computed(() => props.account?.platform === 'anthropic')
const isAntigravity = computed(() => props.account?.platform === 'antigravity')
// Computed - current OAuth state based on platform
const currentAuthUrl = computed(() => {
if (isOpenAI.value) return openaiOAuth.authUrl.value
if (isGemini.value) return geminiOAuth.authUrl.value
if (isAntigravity.value) return antigravityOAuth.authUrl.value
return claudeOAuth.authUrl.value
})
const currentSessionId = computed(() => {
if (isOpenAI.value) return openaiOAuth.sessionId.value
if (isGemini.value) return geminiOAuth.sessionId.value
if (isAntigravity.value) return antigravityOAuth.sessionId.value
return claudeOAuth.sessionId.value
})
const currentLoading = computed(() => {
if (isOpenAI.value) return openaiOAuth.loading.value
if (isGemini.value) return geminiOAuth.loading.value
if (isAntigravity.value) return antigravityOAuth.loading.value
return claudeOAuth.loading.value
})
const currentError = computed(() => {
if (isOpenAI.value) return openaiOAuth.error.value
if (isGemini.value) return geminiOAuth.error.value
if (isAntigravity.value) return antigravityOAuth.error.value
return claudeOAuth.error.value
})
// Computed
const isManualInputMethod = computed(() => {
// OpenAI always uses manual input (no cookie auth option)
return isOpenAI.value || isGemini.value || oauthFlowRef.value?.inputMethod === 'manual'
// OpenAI/Gemini/Antigravity always use manual input (no cookie auth option)
return isOpenAI.value || isGemini.value || isAntigravity.value || oauthFlowRef.value?.inputMethod === 'manual'
})
const canExchangeCode = computed(() => {
const authCode = oauthFlowRef.value?.authCode || ''
const sessionId = isOpenAI.value
? openaiOAuth.sessionId.value
: isGemini.value
? geminiOAuth.sessionId.value
: claudeOAuth.sessionId.value
const loading = isOpenAI.value
? openaiOAuth.loading.value
: isGemini.value
? geminiOAuth.loading.value
: claudeOAuth.loading.value
const sessionId = currentSessionId.value
const loading = currentLoading.value
return authCode.trim() && sessionId && !loading
})
@@ -392,6 +395,7 @@ const resetState = () => {
claudeOAuth.resetState()
openaiOAuth.resetState()
geminiOAuth.resetState()
antigravityOAuth.resetState()
oauthFlowRef.value?.reset()
}
@@ -415,6 +419,8 @@ const handleGenerateUrl = async () => {
} else if (isGemini.value) {
const projectId = geminiOAuthType.value === 'code_assist' ? oauthFlowRef.value?.projectId : undefined
await geminiOAuth.generateAuthUrl(props.account.proxy_id, projectId, geminiOAuthType.value)
} else if (isAntigravity.value) {
await antigravityOAuth.generateAuthUrl(props.account.proxy_id)
} else {
await claudeOAuth.generateAuthUrl(addMethod.value, props.account.proxy_id)
}
@@ -492,6 +498,38 @@ const handleExchangeCode = async () => {
geminiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
appStore.showError(geminiOAuth.error.value)
}
} else if (isAntigravity.value) {
// Antigravity OAuth flow
const sessionId = antigravityOAuth.sessionId.value
if (!sessionId) return
const stateFromInput = oauthFlowRef.value?.oauthState || ''
const stateToUse = stateFromInput || antigravityOAuth.state.value
if (!stateToUse) return
const tokenInfo = await antigravityOAuth.exchangeAuthCode({
code: authCode.trim(),
sessionId,
state: stateToUse,
proxyId: props.account.proxy_id
})
if (!tokenInfo) return
const credentials = antigravityOAuth.buildCredentials(tokenInfo)
try {
await adminAPI.accounts.update(props.account.id, {
type: 'oauth',
credentials
})
await adminAPI.accounts.clearError(props.account.id)
appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess'))
emit('reauthorized')
handleClose()
} catch (error: any) {
antigravityOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
appStore.showError(antigravityOAuth.error.value)
}
} else {
// Claude OAuth flow
const sessionId = claudeOAuth.sessionId.value

View File

@@ -58,7 +58,7 @@ const props = defineProps<{
label: string
utilization: number // Percentage (0-100+)
resetsAt?: string | null
color: 'indigo' | 'emerald' | 'purple'
color: 'indigo' | 'emerald' | 'purple' | 'amber'
windowStats?: WindowStats | null
}>()
@@ -69,7 +69,8 @@ const labelClass = computed(() => {
const colors = {
indigo: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300',
emerald: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
purple: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300'
purple: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300',
amber: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300'
}
return colors[props.color]
})

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,6 +63,13 @@ const filteredGroups = computed(() => {
if (!props.platform) {
return props.groups
}
// 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)
})

View File

@@ -15,6 +15,10 @@
<svg v-else-if="platform === 'gemini'" :class="sizeClass" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2l1.89 7.2L21 12l-7.11 2.8L12 22l-1.89-7.2L3 12l7.11-2.8L12 2z" />
</svg>
<!-- Antigravity logo (cloud) -->
<svg v-else-if="platform === 'antigravity'" :class="sizeClass" viewBox="0 0 24 24" fill="currentColor">
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z" />
</svg>
<!-- Fallback: generic platform icon -->
<svg v-else :class="sizeClass" fill="currentColor" viewBox="0 0 24 24">
<path

View File

@@ -72,6 +72,7 @@ const props = defineProps<Props>()
const platformLabel = computed(() => {
if (props.platform === 'anthropic') return 'Anthropic'
if (props.platform === 'openai') return 'OpenAI'
if (props.platform === 'antigravity') return 'Antigravity'
return 'Gemini'
})
@@ -95,6 +96,9 @@ const platformClass = computed(() => {
if (props.platform === 'openai') {
return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
}
if (props.platform === 'antigravity') {
return 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
}
return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
})
@@ -105,6 +109,9 @@ const typeClass = computed(() => {
if (props.platform === 'openai') {
return 'bg-emerald-100 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400'
}
if (props.platform === 'antigravity') {
return 'bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400'
}
return 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
})
</script>

View File

@@ -0,0 +1,115 @@
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin'
import type { AntigravityTokenInfo } from '@/api/admin/antigravity'
export function useAntigravityOAuth() {
const appStore = useAppStore()
const { t } = useI18n()
const authUrl = ref('')
const sessionId = ref('')
const state = ref('')
const loading = ref(false)
const error = ref('')
const resetState = () => {
authUrl.value = ''
sessionId.value = ''
state.value = ''
loading.value = false
error.value = ''
}
const generateAuthUrl = async (proxyId: number | null | undefined): Promise<boolean> => {
loading.value = true
authUrl.value = ''
sessionId.value = ''
state.value = ''
error.value = ''
try {
const payload: Record<string, unknown> = {}
if (proxyId) payload.proxy_id = proxyId
const response = await adminAPI.antigravity.generateAuthUrl(payload as any)
authUrl.value = response.auth_url
sessionId.value = response.session_id
state.value = response.state
return true
} catch (err: any) {
error.value =
err.response?.data?.detail || t('admin.accounts.oauth.antigravity.failedToGenerateUrl')
appStore.showError(error.value)
return false
} finally {
loading.value = false
}
}
const exchangeAuthCode = async (params: {
code: string
sessionId: string
state: string
proxyId?: number | null
}): Promise<AntigravityTokenInfo | null> => {
const code = params.code?.trim()
if (!code || !params.sessionId || !params.state) {
error.value = t('admin.accounts.oauth.antigravity.missingExchangeParams')
return null
}
loading.value = true
error.value = ''
try {
const payload: Record<string, unknown> = {
session_id: params.sessionId,
state: params.state,
code
}
if (params.proxyId) payload.proxy_id = params.proxyId
const tokenInfo = await adminAPI.antigravity.exchangeCode(payload as any)
return tokenInfo as AntigravityTokenInfo
} catch (err: any) {
error.value =
err.response?.data?.detail || t('admin.accounts.oauth.antigravity.failedToExchangeCode')
appStore.showError(error.value)
return null
} finally {
loading.value = false
}
}
const buildCredentials = (tokenInfo: AntigravityTokenInfo): Record<string, unknown> => {
let expiresAt: string | undefined
if (typeof tokenInfo.expires_at === 'number' && Number.isFinite(tokenInfo.expires_at)) {
expiresAt = Math.floor(tokenInfo.expires_at).toString()
} else if (typeof tokenInfo.expires_at === 'string' && tokenInfo.expires_at.trim()) {
expiresAt = tokenInfo.expires_at.trim()
}
return {
access_token: tokenInfo.access_token,
refresh_token: tokenInfo.refresh_token,
token_type: tokenInfo.token_type,
expires_at: expiresAt,
project_id: tokenInfo.project_id,
email: tokenInfo.email
}
}
return {
authUrl,
sessionId,
state,
loading,
error,
resetState,
generateAuthUrl,
exchangeAuthCode,
buildCredentials
}
}

View File

@@ -33,6 +33,7 @@ export default {
soon: 'Soon',
claude: 'Claude',
gemini: 'Gemini',
antigravity: 'Antigravity',
more: 'More'
},
footer: {
@@ -842,14 +843,16 @@ export default {
anthropic: 'Anthropic',
claude: 'Claude',
openai: 'OpenAI',
gemini: 'Gemini'
gemini: 'Gemini',
antigravity: 'Antigravity'
},
types: {
oauth: 'OAuth',
chatgptOauth: 'ChatGPT OAuth',
responsesApi: 'Responses API',
googleOauth: 'Google OAuth',
codeAssist: 'Code Assist'
codeAssist: 'Code Assist',
antigravityOauth: 'Antigravity OAuth'
},
columns: {
name: 'Name',
@@ -959,6 +962,10 @@ export default {
priority: 'Priority',
priorityHint: 'Higher priority accounts are used first',
higherPriorityFirst: 'Higher value means higher priority',
mixedScheduling: 'Mixed Scheduling',
mixedSchedulingHint: 'Enable to participate in Anthropic/Gemini group scheduling',
mixedSchedulingTooltip:
'When enabled, this account can be scheduled by /v1/messages and /v1beta endpoints. Otherwise, it will only be scheduled by /antigravity. Note: Anthropic Claude and Antigravity Claude cannot be mixed in the same context. Please manage groups carefully when enabled.',
creating: 'Creating...',
updating: 'Updating...',
accountCreated: 'Account created successfully',
@@ -1083,7 +1090,28 @@ export default {
'AI Studio OAuth is not configured: set GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET and add Redirect URI: http://localhost:1455/auth/callback (Consent screen scopes must include https://www.googleapis.com/auth/generative-language.retriever)',
aiStudioNotConfigured:
'AI Studio OAuth is not configured: set GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET and add Redirect URI: http://localhost:1455/auth/callback'
}
},
// Antigravity specific
antigravity: {
title: 'Antigravity Account Authorization',
followSteps: 'Follow these steps to authorize your Antigravity account:',
step1GenerateUrl: 'Generate the authorization URL',
generateAuthUrl: 'Generate Auth URL',
step2OpenUrl: 'Open the URL in your browser and complete authorization',
openUrlDesc: 'Open the authorization URL in a new tab, log in to your Google account and authorize.',
importantNotice:
'<strong>Important:</strong> The page may take a while to load after authorization. Please wait patiently. When the browser address bar shows <code>http://localhost...</code>, authorization is complete.',
step3EnterCode: 'Enter Authorization URL or Code',
authCodeDesc:
'After authorization, when the page URL becomes <code>http://localhost:xxx/auth/callback?code=...</code>:',
authCode: 'Authorization URL or Code',
authCodePlaceholder:
'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value',
authCodeHint: 'You can copy the entire URL or just the code parameter value, the system will auto-detect',
failedToGenerateUrl: 'Failed to generate Antigravity auth URL',
missingExchangeParams: 'Missing code, session ID, or state',
failedToExchangeCode: 'Failed to exchange Antigravity auth code'
}
},
// Gemini specific (platform-wide)
gemini: {
@@ -1098,6 +1126,7 @@ export default {
claudeCodeAccount: 'Claude Code Account',
openaiAccount: 'OpenAI Account',
geminiAccount: 'Gemini Account',
antigravityAccount: 'Antigravity Account',
inputMethod: 'Input Method',
reAuthorizedSuccess: 'Account re-authorized successfully',
// Test Modal
@@ -1155,7 +1184,16 @@ export default {
noData: 'No usage data available for this account'
},
usageWindow: {
statsTitle: '5-Hour Window Usage Statistics'
statsTitle: '5-Hour Window Usage Statistics',
gemini3Pro: 'G3P',
gemini3Flash: 'G3F',
gemini3Image: 'G3I',
claude45: 'C4.5'
},
tier: {
free: 'Free',
pro: 'Pro',
ultra: 'Ultra'
}
},

View File

@@ -30,6 +30,7 @@ export default {
soon: '即将推出',
claude: 'Claude',
gemini: 'Gemini',
antigravity: 'Antigravity',
more: '更多'
},
footer: {
@@ -962,7 +963,8 @@ export default {
claude: 'Claude',
openai: 'OpenAI',
anthropic: 'Anthropic',
gemini: 'Gemini'
gemini: 'Gemini',
antigravity: 'Antigravity'
},
types: {
oauth: 'OAuth',
@@ -970,6 +972,7 @@ export default {
responsesApi: 'Responses API',
googleOauth: 'Google OAuth',
codeAssist: 'Code Assist',
antigravityOauth: 'Antigravity OAuth',
api_key: 'API Key',
cookie: 'Cookie'
},
@@ -980,7 +983,16 @@ export default {
cooldown: '冷却中'
},
usageWindow: {
statsTitle: '5小时窗口用量统计'
statsTitle: '5小时窗口用量统计',
gemini3Pro: 'G3P',
gemini3Flash: 'G3F',
gemini3Image: 'G3I',
claude45: 'C4.5'
},
tier: {
free: 'Free',
pro: 'Pro',
ultra: 'Ultra'
},
form: {
nameLabel: '账号名称',
@@ -1095,6 +1107,10 @@ export default {
priority: '优先级',
priorityHint: '优先级越高的账号优先使用',
higherPriorityFirst: '数值越高优先级越高',
mixedScheduling: '混合调度',
mixedSchedulingHint: '启用后可参与 Anthropic/Gemini 分组的调度',
mixedSchedulingTooltip:
'开启后,该账户可被 /v1/messages 及 /v1beta 端点调度,否则只被 /antigravity 调度。注意Anthropic Claude 和 Antigravity Claude 无法在同个上下文中混合使用,开启后请自行做好分组管理。',
creating: '创建中...',
updating: '更新中...',
accountCreated: '账号创建成功',
@@ -1205,7 +1221,28 @@ export default {
aiStudioNotConfiguredShort: '未配置',
aiStudioNotConfiguredTip: 'AI Studio OAuth 未配置:请先设置 GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET并在 Google OAuth Client 添加 Redirect URIhttp://localhost:1455/auth/callbackConsent Screen scopes 需包含 https://www.googleapis.com/auth/generative-language.retriever',
aiStudioNotConfigured: 'AI Studio OAuth 未配置:请先设置 GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET并在 Google OAuth Client 添加 Redirect URIhttp://localhost:1455/auth/callback'
}
},
// Antigravity specific
antigravity: {
title: 'Antigravity 账户授权',
followSteps: '请按照以下步骤完成 Antigravity 账户的授权:',
step1GenerateUrl: '生成授权链接',
generateAuthUrl: '生成授权链接',
step2OpenUrl: '在浏览器中打开链接并完成授权',
openUrlDesc: '请在新标签页中打开授权链接,登录您的 Google 账户并授权。',
importantNotice:
'<strong>重要提示:</strong>授权后页面可能会加载较长时间,请耐心等待。当浏览器地址栏变为 <code>http://localhost...</code> 开头时,表示授权已完成。',
step3EnterCode: '输入授权链接或 Code',
authCodeDesc:
'授权完成后,当页面地址变为 <code>http://localhost:xxx/auth/callback?code=...</code> 时:',
authCode: '授权链接或 Code',
authCodePlaceholder:
'方式1复制完整的链接\n(http://localhost:xxx/auth/callback?code=...)\n方式2仅复制 code 参数的值',
authCodeHint: '您可以直接复制整个链接或仅复制 code 参数值,系统会自动识别',
failedToGenerateUrl: '生成 Antigravity 授权链接失败',
missingExchangeParams: '缺少 code / session_id / state',
failedToExchangeCode: 'Antigravity 授权码兑换失败'
}
},
// Gemini specific (platform-wide)
gemini: {
@@ -1219,6 +1256,7 @@ export default {
claudeCodeAccount: 'Claude Code 账号',
openaiAccount: 'OpenAI 账号',
geminiAccount: 'Gemini 账号',
antigravityAccount: 'Antigravity 账号',
inputMethod: '输入方式',
reAuthorizedSuccess: '账号重新授权成功',
// Test Modal

View File

@@ -224,7 +224,7 @@ export interface PaginationConfig {
// ==================== API Key & Group Types ====================
export type GroupPlatform = 'anthropic' | 'openai' | 'gemini'
export type GroupPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity'
export type SubscriptionType = 'standard' | 'subscription'
@@ -260,7 +260,7 @@ export interface ApiKey {
export interface CreateApiKeyRequest {
name: string
group_id?: number | null
custom_key?: string // 可选的自定义API Key
custom_key?: string // Optional custom API Key
}
export interface UpdateApiKeyRequest {
@@ -288,7 +288,7 @@ export interface UpdateGroupRequest {
// ==================== Account & Proxy Types ====================
export type AccountPlatform = 'anthropic' | 'openai' | 'gemini'
export type AccountPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity'
export type AccountType = 'oauth' | 'setup-token' | 'apikey'
export type OAuthAddMethod = 'oauth' | 'setup-token'
export type ProxyProtocol = 'http' | 'https' | 'socks5'
@@ -396,7 +396,7 @@ export interface CreateAccountRequest {
platform: AccountPlatform
type: AccountType
credentials: Record<string, unknown>
extra?: Record<string, string>
extra?: Record<string, unknown>
proxy_id?: number | null
concurrency?: number
priority?: number
@@ -407,7 +407,7 @@ export interface UpdateAccountRequest {
name?: string
type?: AccountType
credentials?: Record<string, unknown>
extra?: Record<string, string>
extra?: Record<string, unknown>
proxy_id?: number | null
concurrency?: number
priority?: number

View File

@@ -421,6 +421,21 @@
>{{ t('home.providers.supported') }}</span
>
</div>
<!-- Antigravity - Supported -->
<div
class="flex items-center gap-2 rounded-xl border border-primary-200 bg-white/60 px-5 py-3 ring-1 ring-primary-500/20 backdrop-blur-sm dark:border-primary-800 dark:bg-dark-800/60"
>
<div
class="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-rose-500 to-pink-600"
>
<span class="text-xs font-bold text-white">A</span>
</div>
<span class="text-sm font-medium text-gray-700 dark:text-dark-200">{{ t('home.providers.antigravity') }}</span>
<span
class="rounded bg-primary-100 px-1.5 py-0.5 text-[10px] font-medium text-primary-600 dark:bg-primary-900/30 dark:text-primary-400"
>{{ t('home.providers.supported') }}</span
>
</div>
<!-- More - Coming Soon -->
<div
class="flex items-center gap-2 rounded-xl border border-gray-200/50 bg-white/40 px-5 py-3 opacity-60 backdrop-blur-sm dark:border-dark-700/50 dark:bg-dark-800/40"

View File

@@ -559,7 +559,8 @@ const platformOptions = computed(() => [
{ value: '', label: t('admin.accounts.allPlatforms') },
{ value: 'anthropic', label: t('admin.accounts.platforms.anthropic') },
{ value: 'openai', label: t('admin.accounts.platforms.openai') },
{ value: 'gemini', label: t('admin.accounts.platforms.gemini') }
{ value: 'gemini', label: t('admin.accounts.platforms.gemini') },
{ value: 'antigravity', label: t('admin.accounts.platforms.antigravity') }
])
const typeOptions = computed(() => [

View File

@@ -82,11 +82,21 @@
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
: value === 'openai'
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
: value === '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'
]"
>
<PlatformIcon :platform="value" size="xs" />
{{ value === 'anthropic' ? 'Anthropic' : value === 'openai' ? 'OpenAI' : 'Gemini' }}
{{
value === 'anthropic'
? 'Anthropic'
: value === 'openai'
? 'OpenAI'
: value === 'antigravity'
? 'Antigravity'
: 'Gemini'
}}
</span>
</template>
@@ -694,14 +704,16 @@ const exclusiveOptions = computed(() => [
const platformOptions = computed(() => [
{ value: 'anthropic', label: 'Anthropic' },
{ value: 'openai', label: 'OpenAI' },
{ value: 'gemini', label: 'Gemini' }
{ value: 'gemini', label: 'Gemini' },
{ value: 'antigravity', label: 'Antigravity' }
])
const platformFilterOptions = computed(() => [
{ value: '', label: t('admin.groups.allPlatforms') },
{ value: 'anthropic', label: 'Anthropic' },
{ value: 'openai', label: 'OpenAI' },
{ value: 'gemini', label: 'Gemini' }
{ value: 'gemini', label: 'Gemini' },
{ value: 'antigravity', label: 'Antigravity' }
])
const editStatusOptions = computed(() => [