mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-03 15:02:13 +08:00
Merge pull request #959 from touwaeriol/feat/antigravity-403-detection
feat(antigravity): add 403 forbidden status detection and display
This commit is contained in:
@@ -36,6 +36,10 @@
|
||||
|
||||
<!-- Usage data -->
|
||||
<div v-else-if="usageInfo" class="space-y-1">
|
||||
<!-- API error (degraded response) -->
|
||||
<div v-if="usageInfo.error" class="text-xs text-amber-600 dark:text-amber-400 truncate max-w-[200px]" :title="usageInfo.error">
|
||||
{{ usageInfo.error }}
|
||||
</div>
|
||||
<!-- 5h Window -->
|
||||
<UsageProgressBar
|
||||
v-if="usageInfo.five_hour"
|
||||
@@ -189,8 +193,53 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Forbidden state (403) -->
|
||||
<div v-if="isForbidden" class="space-y-1">
|
||||
<span
|
||||
:class="[
|
||||
'inline-block rounded px-1.5 py-0.5 text-[10px] font-medium',
|
||||
forbiddenBadgeClass
|
||||
]"
|
||||
>
|
||||
{{ forbiddenLabel }}
|
||||
</span>
|
||||
<div v-if="validationURL" class="flex items-center gap-1">
|
||||
<a
|
||||
:href="validationURL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-[10px] text-blue-600 hover:text-blue-800 hover:underline dark:text-blue-400 dark:hover:text-blue-300"
|
||||
:title="t('admin.accounts.openVerification')"
|
||||
>
|
||||
{{ t('admin.accounts.openVerification') }}
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="text-[10px] text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
:title="t('admin.accounts.copyLink')"
|
||||
@click="copyValidationURL"
|
||||
>
|
||||
{{ linkCopied ? t('admin.accounts.linkCopied') : t('admin.accounts.copyLink') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Needs reauth (401) -->
|
||||
<div v-else-if="needsReauth" class="space-y-1">
|
||||
<span class="inline-block rounded px-1.5 py-0.5 text-[10px] font-medium bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300">
|
||||
{{ t('admin.accounts.needsReauth') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Degraded error (non-403, non-401) -->
|
||||
<div v-else-if="usageInfo?.error" class="space-y-1">
|
||||
<span class="inline-block rounded px-1.5 py-0.5 text-[10px] font-medium bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300">
|
||||
{{ usageErrorLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="space-y-1.5">
|
||||
<div v-else-if="loading" class="space-y-1.5">
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||
<div class="h-1.5 w-8 animate-pulse rounded-full bg-gray-200 dark:bg-gray-700"></div>
|
||||
@@ -816,6 +865,51 @@ const hasIneligibleTiers = computed(() => {
|
||||
return Array.isArray(ineligibleTiers) && ineligibleTiers.length > 0
|
||||
})
|
||||
|
||||
// Antigravity 403 forbidden 状态
|
||||
const isForbidden = computed(() => !!usageInfo.value?.is_forbidden)
|
||||
const forbiddenType = computed(() => usageInfo.value?.forbidden_type || 'forbidden')
|
||||
const validationURL = computed(() => usageInfo.value?.validation_url || '')
|
||||
|
||||
// 需要重新授权(401)
|
||||
const needsReauth = computed(() => !!usageInfo.value?.needs_reauth)
|
||||
|
||||
// 降级错误标签(rate_limited / network_error)
|
||||
const usageErrorLabel = computed(() => {
|
||||
const code = usageInfo.value?.error_code
|
||||
if (code === 'rate_limited') return t('admin.accounts.rateLimited')
|
||||
return t('admin.accounts.usageError')
|
||||
})
|
||||
|
||||
const forbiddenLabel = computed(() => {
|
||||
switch (forbiddenType.value) {
|
||||
case 'validation':
|
||||
return t('admin.accounts.forbiddenValidation')
|
||||
case 'violation':
|
||||
return t('admin.accounts.forbiddenViolation')
|
||||
default:
|
||||
return t('admin.accounts.forbidden')
|
||||
}
|
||||
})
|
||||
|
||||
const forbiddenBadgeClass = computed(() => {
|
||||
if (forbiddenType.value === 'validation') {
|
||||
return 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300'
|
||||
}
|
||||
return 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300'
|
||||
})
|
||||
|
||||
const linkCopied = ref(false)
|
||||
const copyValidationURL = async () => {
|
||||
if (!validationURL.value) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(validationURL.value)
|
||||
linkCopied.value = true
|
||||
setTimeout(() => { linkCopied.value = false }, 2000)
|
||||
} catch {
|
||||
// fallback: ignore
|
||||
}
|
||||
}
|
||||
|
||||
const loadUsage = async () => {
|
||||
if (!shouldFetchUsage.value) return
|
||||
|
||||
|
||||
@@ -84,9 +84,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.table-scroll-container :deep(th) {
|
||||
/* 表头高度和文字加粗优化 */
|
||||
@apply px-5 py-4 text-left text-sm font-bold text-gray-900 dark:text-white border-b border-gray-200 dark:border-dark-700;
|
||||
@apply uppercase tracking-wider; /* 让表头更有设计感 */
|
||||
@apply px-5 py-4 text-left text-sm font-medium text-gray-600 dark:text-dark-300 border-b border-gray-200 dark:border-dark-700;
|
||||
}
|
||||
|
||||
.table-scroll-container :deep(td) {
|
||||
|
||||
@@ -2555,7 +2555,16 @@ export default {
|
||||
unlimited: 'Unlimited'
|
||||
},
|
||||
ineligibleWarning:
|
||||
'This account is not eligible for Antigravity, but API forwarding still works. Use at your own risk.'
|
||||
'This account is not eligible for Antigravity, but API forwarding still works. Use at your own risk.',
|
||||
forbidden: 'Forbidden',
|
||||
forbiddenValidation: 'Verification Required',
|
||||
forbiddenViolation: 'Violation Ban',
|
||||
openVerification: 'Open Verification Link',
|
||||
copyLink: 'Copy Link',
|
||||
linkCopied: 'Link Copied',
|
||||
needsReauth: 'Re-auth Required',
|
||||
rateLimited: 'Rate Limited',
|
||||
usageError: 'Fetch Error'
|
||||
},
|
||||
|
||||
// Scheduled Tests
|
||||
|
||||
@@ -1992,6 +1992,15 @@ export default {
|
||||
},
|
||||
ineligibleWarning:
|
||||
'该账号无 Antigravity 使用权限,但仍能进行 API 转发。继续使用请自行承担风险。',
|
||||
forbidden: '已封禁',
|
||||
forbiddenValidation: '需要验证',
|
||||
forbiddenViolation: '违规封禁',
|
||||
openVerification: '打开验证链接',
|
||||
copyLink: '复制链接',
|
||||
linkCopied: '链接已复制',
|
||||
needsReauth: '需要重新授权',
|
||||
rateLimited: '限流中',
|
||||
usageError: '获取失败',
|
||||
form: {
|
||||
nameLabel: '账号名称',
|
||||
namePlaceholder: '请输入账号名称',
|
||||
|
||||
@@ -769,6 +769,21 @@ export interface AccountUsageInfo {
|
||||
gemini_pro_minute?: UsageProgress | null
|
||||
gemini_flash_minute?: UsageProgress | null
|
||||
antigravity_quota?: Record<string, AntigravityModelQuota> | null
|
||||
// Antigravity 403 forbidden 状态
|
||||
is_forbidden?: boolean
|
||||
forbidden_reason?: string
|
||||
forbidden_type?: string // "validation" | "violation" | "forbidden"
|
||||
validation_url?: string // 验证/申诉链接
|
||||
|
||||
// 状态标记(后端自动推导)
|
||||
needs_verify?: boolean // 需要人工验证(forbidden_type=validation)
|
||||
is_banned?: boolean // 账号被封(forbidden_type=violation)
|
||||
needs_reauth?: boolean // token 失效需重新授权(401)
|
||||
|
||||
// 机器可读错误码:forbidden / unauthenticated / rate_limited / network_error
|
||||
error_code?: string
|
||||
|
||||
error?: string // usage 获取失败时的错误信息
|
||||
}
|
||||
|
||||
// OpenAI Codex usage snapshot (from response headers)
|
||||
|
||||
@@ -171,7 +171,15 @@
|
||||
<span v-else class="text-sm text-gray-400 dark:text-dark-500">-</span>
|
||||
</template>
|
||||
<template #cell-platform_type="{ row }">
|
||||
<PlatformTypeBadge :platform="row.platform" :type="row.type" :plan-type="row.credentials?.plan_type" :privacy-mode="row.extra?.privacy_mode" />
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
<PlatformTypeBadge :platform="row.platform" :type="row.type" :plan-type="row.credentials?.plan_type" :privacy-mode="row.extra?.privacy_mode" />
|
||||
<span
|
||||
v-if="getAntigravityTierLabel(row)"
|
||||
:class="['inline-block rounded px-1.5 py-0.5 text-[10px] font-medium', getAntigravityTierClass(row)]"
|
||||
>
|
||||
{{ getAntigravityTierLabel(row) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell-capacity="{ row }">
|
||||
<AccountCapacityCell :account="row" />
|
||||
@@ -794,6 +802,40 @@ const { pause: pauseAutoRefresh, resume: resumeAutoRefresh } = useIntervalFn(
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
// Antigravity 订阅等级辅助函数
|
||||
function getAntigravityTierFromRow(row: any): string | null {
|
||||
if (row.platform !== 'antigravity') return null
|
||||
const extra = row.extra as Record<string, unknown> | undefined
|
||||
if (!extra) return null
|
||||
const lca = extra.load_code_assist as Record<string, unknown> | undefined
|
||||
if (!lca) return null
|
||||
const paid = lca.paidTier as Record<string, unknown> | undefined
|
||||
if (paid && typeof paid.id === 'string') return paid.id
|
||||
const current = lca.currentTier as Record<string, unknown> | undefined
|
||||
if (current && typeof current.id === 'string') return current.id
|
||||
return null
|
||||
}
|
||||
|
||||
function getAntigravityTierLabel(row: any): string | null {
|
||||
const tier = getAntigravityTierFromRow(row)
|
||||
switch (tier) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
function getAntigravityTierClass(row: any): string {
|
||||
const tier = getAntigravityTierFromRow(row)
|
||||
switch (tier) {
|
||||
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 ''
|
||||
}
|
||||
}
|
||||
|
||||
// All available columns
|
||||
const allColumns = computed(() => {
|
||||
const c = [
|
||||
|
||||
Reference in New Issue
Block a user