feat(antigravity): 支持 Refresh Token 批量导入创建 OAuth 账号

后端新增 ValidateRefreshToken service 方法和 POST /oauth/refresh-token 端点,
前端新增 API/Composable/UI 集成,OAuthAuthorizationFlow i18n 动态化,
支持在 Antigravity 创建账号时批量粘贴 Refresh Token 自动验证并创建账号。
This commit is contained in:
Tian
2026-02-10 23:57:18 +08:00
parent ae6fed15cc
commit c8f87a9c92
9 changed files with 235 additions and 17 deletions

View File

@@ -1647,12 +1647,12 @@
:show-proxy-warning="form.platform !== 'openai' && !!form.proxy_id"
:allow-multiple="form.platform === 'anthropic'"
:show-cookie-option="form.platform === 'anthropic'"
:show-refresh-token-option="form.platform === 'openai'"
:show-refresh-token-option="form.platform === 'openai' || form.platform === 'antigravity'"
:platform="form.platform"
:show-project-id="geminiOAuthType === 'code_assist'"
@generate-url="handleGenerateUrl"
@cookie-auth="handleCookieAuth"
@validate-refresh-token="handleOpenAIValidateRT"
@validate-refresh-token="handleValidateRefreshToken"
/>
</div>
@@ -2802,6 +2802,14 @@ const handleGenerateUrl = async () => {
}
}
const handleValidateRefreshToken = (rt: string) => {
if (form.platform === 'openai') {
handleOpenAIValidateRT(rt)
} else if (form.platform === 'antigravity') {
handleAntigravityValidateRT(rt)
}
}
const formatDateTimeLocal = formatDateTimeLocalInput
const parseDateTimeLocal = parseDateTimeLocalInput
@@ -2950,6 +2958,95 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
}
}
// Antigravity 手动 RT 批量验证和创建
const handleAntigravityValidateRT = async (refreshTokenInput: string) => {
if (!refreshTokenInput.trim()) return
// Parse multiple refresh tokens (one per line)
const refreshTokens = refreshTokenInput
.split('\n')
.map((rt) => rt.trim())
.filter((rt) => rt)
if (refreshTokens.length === 0) {
antigravityOAuth.error.value = t('admin.accounts.oauth.antigravity.pleaseEnterRefreshToken')
return
}
antigravityOAuth.loading.value = true
antigravityOAuth.error.value = ''
let successCount = 0
let failedCount = 0
const errors: string[] = []
try {
for (let i = 0; i < refreshTokens.length; i++) {
try {
const tokenInfo = await antigravityOAuth.validateRefreshToken(
refreshTokens[i],
form.proxy_id
)
if (!tokenInfo) {
failedCount++
errors.push(`#${i + 1}: ${antigravityOAuth.error.value || 'Validation failed'}`)
antigravityOAuth.error.value = ''
continue
}
const credentials = antigravityOAuth.buildCredentials(tokenInfo)
// Generate account name with index for batch
const accountName = refreshTokens.length > 1 ? `${form.name} #${i + 1}` : form.name
// Note: Antigravity doesn't have buildExtraInfo, so we pass empty extra or rely on credentials
await adminAPI.accounts.create({
name: accountName,
notes: form.notes,
platform: 'antigravity',
type: 'oauth',
credentials,
extra: {},
proxy_id: form.proxy_id,
concurrency: form.concurrency,
priority: form.priority,
rate_multiplier: form.rate_multiplier,
group_ids: form.group_ids,
expires_at: form.expires_at,
auto_pause_on_expired: autoPauseOnExpired.value
})
successCount++
} catch (error: any) {
failedCount++
const errMsg = error.response?.data?.detail || error.message || 'Unknown error'
errors.push(`#${i + 1}: ${errMsg}`)
}
}
// Show results
if (successCount > 0 && failedCount === 0) {
appStore.showSuccess(
refreshTokens.length > 1
? t('admin.accounts.oauth.batchSuccess', { count: successCount })
: t('admin.accounts.accountCreated')
)
emit('created')
handleClose()
} else if (successCount > 0 && failedCount > 0) {
appStore.showWarning(
t('admin.accounts.oauth.batchPartialSuccess', { success: successCount, failed: failedCount })
)
antigravityOAuth.error.value = errors.join('\n')
emit('created')
} else {
antigravityOAuth.error.value = errors.join('\n')
appStore.showError(t('admin.accounts.oauth.batchFailed'))
}
} finally {
antigravityOAuth.loading.value = false
}
}
// Gemini OAuth 授权码兑换
const handleGeminiExchange = async (authCode: string) => {
if (!authCode.trim() || !geminiOAuth.sessionId.value) return

View File

@@ -45,19 +45,19 @@
class="text-blue-600 focus:ring-blue-500"
/>
<span class="text-sm text-blue-900 dark:text-blue-200">{{
t('admin.accounts.oauth.openai.refreshTokenAuth')
t(getOAuthKey('refreshTokenAuth'))
}}</span>
</label>
</div>
</div>
<!-- Refresh Token Input (OpenAI only) -->
<!-- Refresh Token Input (OpenAI / Antigravity) -->
<div v-if="inputMethod === 'refresh_token'" class="space-y-4">
<div
class="rounded-lg border border-blue-300 bg-white/80 p-4 dark:border-blue-600 dark:bg-gray-800/80"
>
<p class="mb-3 text-sm text-blue-700 dark:text-blue-300">
{{ t('admin.accounts.oauth.openai.refreshTokenDesc') }}
{{ t(getOAuthKey('refreshTokenDesc')) }}
</p>
<!-- Refresh Token Input -->
@@ -78,7 +78,7 @@
v-model="refreshTokenInput"
rows="3"
class="input w-full resize-y font-mono text-sm"
:placeholder="t('admin.accounts.oauth.openai.refreshTokenPlaceholder')"
:placeholder="t(getOAuthKey('refreshTokenPlaceholder'))"
></textarea>
<p
v-if="parsedRefreshTokenCount > 1"
@@ -128,8 +128,8 @@
<Icon v-else name="sparkles" size="sm" class="mr-2" />
{{
loading
? t('admin.accounts.oauth.openai.validating')
: t('admin.accounts.oauth.openai.validateAndCreate')
? t(getOAuthKey('validating'))
: t(getOAuthKey('validateAndCreate'))
}}
</button>
</div>