mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix(frontend): 为配额自动刷新加入 5 分钟冷却并优化 OAuth org 徽章显示
- 新增 quotaAutoRefreshCooldown 工具,按 provider 维度限制后台自动刷新频率,手动触发(刷新 token、变更 key)通过 ignoreCooldown 绕过 - OAuth org 徽章去除 org- 前缀后再缩略,避免 org:org-xx 的重复前缀,并缩小字号高度以适配紧凑布局
This commit is contained in:
@@ -308,7 +308,7 @@
|
||||
<Badge
|
||||
v-if="getOAuthOrgBadge(key)"
|
||||
variant="secondary"
|
||||
class="text-[10px] px-1.5 py-0 shrink-0"
|
||||
class="text-[9px] px-1 py-0 h-4 shrink-0"
|
||||
:title="getOAuthOrgBadge(key)?.title"
|
||||
>
|
||||
{{ getOAuthOrgBadge(key)?.label }}
|
||||
@@ -1164,6 +1164,10 @@ import type {
|
||||
} from '@/api/endpoints/types'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import { isOAuthAccountProviderType, isKeyManagedProviderType } from '../utils/providerTypeUtils'
|
||||
import {
|
||||
isProviderQuotaAutoRefreshCoolingDown,
|
||||
markProviderQuotaAutoRefreshAttempt,
|
||||
} from '../utils/quotaAutoRefreshCooldown'
|
||||
import { getOAuthOrgBadge } from '@/utils/oauthIdentity'
|
||||
import { getOAuthRefreshFeedback } from '@/utils/oauthRefreshFeedback'
|
||||
import {
|
||||
@@ -1724,7 +1728,7 @@ async function handleRefreshOAuth(key: EndpointAPIKey) {
|
||||
}
|
||||
// Antigravity:token 刷新后可能完成了账号激活,触发配额获取
|
||||
// (不 emit('refresh'),避免触发全局 provider 余额刷新)
|
||||
void autoRefreshQuotaInBackground()
|
||||
void autoRefreshQuotaInBackground({ ignoreCooldown: true })
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, 'Token 刷新失败'), '错误')
|
||||
} finally {
|
||||
@@ -2246,8 +2250,9 @@ function applyQuotaResults(
|
||||
}
|
||||
|
||||
// 通用的自动刷新配额函数(支持 Codex、Antigravity 和 Kiro)
|
||||
async function autoRefreshQuotaInBackground() {
|
||||
if (!props.providerId) return
|
||||
async function autoRefreshQuotaInBackground(options: { ignoreCooldown?: boolean } = {}) {
|
||||
const providerId = props.providerId
|
||||
if (!providerId) return
|
||||
if (refreshingQuota.value) return
|
||||
|
||||
const providerType = provider.value?.provider_type
|
||||
@@ -2263,6 +2268,7 @@ async function autoRefreshQuotaInBackground() {
|
||||
shouldRefresh = shouldAutoRefreshKiroQuota()
|
||||
}
|
||||
if (!shouldRefresh) return
|
||||
if (!options.ignoreCooldown && isProviderQuotaAutoRefreshCoolingDown(providerId)) return
|
||||
|
||||
let hadCachedQuota = false
|
||||
if (providerType === 'codex') {
|
||||
@@ -2274,8 +2280,9 @@ async function autoRefreshQuotaInBackground() {
|
||||
}
|
||||
|
||||
refreshingQuota.value = true
|
||||
markProviderQuotaAutoRefreshAttempt(providerId)
|
||||
try {
|
||||
const result = await refreshProviderQuota(props.providerId)
|
||||
const result = await refreshProviderQuota(providerId)
|
||||
const applied = applyQuotaResults(result.results)
|
||||
if (result.success <= 0 && applied === 0 && !hadCachedQuota && providerType === 'antigravity') {
|
||||
showError('没有获取到配额信息(请检查账号是否已授权、project_id 是否存在)', '提示')
|
||||
@@ -2317,7 +2324,7 @@ async function handleKeyChanged() {
|
||||
await Promise.all([loadEndpoints(), loadMappingPreview()])
|
||||
emit('refresh')
|
||||
// 添加/修改 key 后自动获取 Antigravity 配额(新 key 的 upstream_metadata 为空)
|
||||
void autoRefreshQuotaInBackground()
|
||||
void autoRefreshQuotaInBackground({ ignoreCooldown: true })
|
||||
}
|
||||
|
||||
// 切换密钥启用状态
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
AUTO_QUOTA_REFRESH_COOLDOWN_SECONDS,
|
||||
isProviderQuotaAutoRefreshCoolingDown,
|
||||
markProviderQuotaAutoRefreshAttempt,
|
||||
resetProviderQuotaAutoRefreshCooldownForTests,
|
||||
} from '../quotaAutoRefreshCooldown'
|
||||
|
||||
describe('quota auto refresh cooldown', () => {
|
||||
afterEach(() => {
|
||||
resetProviderQuotaAutoRefreshCooldownForTests()
|
||||
})
|
||||
|
||||
it('starts cooldown after recording an auto refresh attempt', () => {
|
||||
markProviderQuotaAutoRefreshAttempt('provider-1', 1_000)
|
||||
|
||||
expect(isProviderQuotaAutoRefreshCoolingDown('provider-1', 1_000)).toBe(true)
|
||||
expect(
|
||||
isProviderQuotaAutoRefreshCoolingDown(
|
||||
'provider-1',
|
||||
1_000 + AUTO_QUOTA_REFRESH_COOLDOWN_SECONDS - 1,
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('expires cooldown after the configured window', () => {
|
||||
markProviderQuotaAutoRefreshAttempt('provider-1', 1_000)
|
||||
|
||||
expect(
|
||||
isProviderQuotaAutoRefreshCoolingDown(
|
||||
'provider-1',
|
||||
1_000 + AUTO_QUOTA_REFRESH_COOLDOWN_SECONDS,
|
||||
),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('tracks cooldown independently per provider', () => {
|
||||
markProviderQuotaAutoRefreshAttempt('provider-1', 1_000)
|
||||
markProviderQuotaAutoRefreshAttempt('provider-2', 1_200)
|
||||
|
||||
expect(isProviderQuotaAutoRefreshCoolingDown('provider-1', 1_301)).toBe(false)
|
||||
expect(isProviderQuotaAutoRefreshCoolingDown('provider-2', 1_301)).toBe(true)
|
||||
})
|
||||
|
||||
it('ignores empty provider ids', () => {
|
||||
markProviderQuotaAutoRefreshAttempt('', 1_000)
|
||||
|
||||
expect(isProviderQuotaAutoRefreshCoolingDown('', 1_001)).toBe(false)
|
||||
expect(isProviderQuotaAutoRefreshCoolingDown(null, 1_001)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
const AUTO_QUOTA_REFRESH_COOLDOWN_SECONDS = 5 * 60
|
||||
|
||||
const lastAutoQuotaRefreshAttemptAtByProvider = new Map<string, number>()
|
||||
|
||||
function normalizeUnixSeconds(value: number): number {
|
||||
return Math.max(Math.floor(value), 0)
|
||||
}
|
||||
|
||||
export function isProviderQuotaAutoRefreshCoolingDown(
|
||||
providerId: string | null | undefined,
|
||||
nowSeconds = Math.floor(Date.now() / 1000),
|
||||
): boolean {
|
||||
const id = String(providerId || '').trim()
|
||||
if (!id) return false
|
||||
|
||||
const lastAttemptAt = lastAutoQuotaRefreshAttemptAtByProvider.get(id)
|
||||
if (lastAttemptAt == null) return false
|
||||
|
||||
return normalizeUnixSeconds(nowSeconds) - lastAttemptAt < AUTO_QUOTA_REFRESH_COOLDOWN_SECONDS
|
||||
}
|
||||
|
||||
export function markProviderQuotaAutoRefreshAttempt(
|
||||
providerId: string | null | undefined,
|
||||
nowSeconds = Math.floor(Date.now() / 1000),
|
||||
): void {
|
||||
const id = String(providerId || '').trim()
|
||||
if (!id) return
|
||||
|
||||
lastAutoQuotaRefreshAttemptAtByProvider.set(id, normalizeUnixSeconds(nowSeconds))
|
||||
}
|
||||
|
||||
export function resetProviderQuotaAutoRefreshCooldownForTests(): void {
|
||||
lastAutoQuotaRefreshAttemptAtByProvider.clear()
|
||||
}
|
||||
|
||||
export { AUTO_QUOTA_REFRESH_COOLDOWN_SECONDS }
|
||||
@@ -15,7 +15,7 @@ describe('getOAuthOrgBadge', () => {
|
||||
|
||||
expect(badge).toEqual({
|
||||
id: 'org-personal-1234',
|
||||
label: 'org:org-pe...1234',
|
||||
label: 'org:person...1234',
|
||||
title: 'name: Workspace Alpha | account_id: acct-demo-001 | account_user_id: user-1__acct-demo-001 | org_id: org-personal-1234 | org_title: Personal',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -22,6 +22,19 @@ function readStr(raw: unknown): string {
|
||||
return typeof raw === 'string' ? raw.trim() : ''
|
||||
}
|
||||
|
||||
function stripOAuthOrganizationPrefix(orgId: string): string {
|
||||
return orgId.replace(/^org[-_:]+/i, '').trim()
|
||||
}
|
||||
|
||||
function formatOAuthOrganizationBadge(orgId: string): string {
|
||||
const compactOrgId = stripOAuthOrganizationPrefix(orgId)
|
||||
const normalized = compactOrgId || orgId
|
||||
if (normalized.length <= 10) {
|
||||
return `org:${normalized}`
|
||||
}
|
||||
return `org:${normalized.slice(0, 6)}...${normalized.slice(-4)}`
|
||||
}
|
||||
|
||||
function formatOAuthAccountBadge(accountId: string): string {
|
||||
return accountId.slice(0, 8)
|
||||
}
|
||||
@@ -67,7 +80,7 @@ export function getOAuthOrgBadge(
|
||||
|
||||
const badgeId = org?.id || accountId || accountUserId || ''
|
||||
const label = org?.id
|
||||
? `org:${formatOAuthIdentityShort(org.id, 6, 4)}`
|
||||
? formatOAuthOrganizationBadge(org.id)
|
||||
: accountId
|
||||
? formatOAuthAccountBadge(accountId)
|
||||
: accountUserId
|
||||
|
||||
Reference in New Issue
Block a user