mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
fix provider pool quota status handling
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { cleanAccountBlockReason, isRefreshFailedReason } from '@/utils/accountBlock'
|
||||
import {
|
||||
cleanAccountBlockReason,
|
||||
isAccountLevelBlockReason,
|
||||
isRefreshFailedReason,
|
||||
} from '@/utils/accountBlock'
|
||||
|
||||
describe('accountBlock helpers', () => {
|
||||
it('detects refresh failure markers even when account block is also present', () => {
|
||||
@@ -18,4 +22,12 @@ describe('accountBlock helpers', () => {
|
||||
),
|
||||
).toBe('工作区已停用 (deactivated_workspace)')
|
||||
})
|
||||
|
||||
it('does not treat refresh failure text as an account block by itself', () => {
|
||||
expect(
|
||||
isAccountLevelBlockReason(
|
||||
'[REFRESH_FAILED] Token 续期失败 (401): token has been invalidated',
|
||||
),
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -87,11 +87,59 @@ describe('providerKeyStatus', () => {
|
||||
)
|
||||
|
||||
expect(status).toEqual({
|
||||
text: '已失效',
|
||||
text: expect.stringMatching(/^续期失败 /),
|
||||
isExpired: false,
|
||||
isExpiringSoon: false,
|
||||
isInvalid: true,
|
||||
isExpiringSoon: expect.any(Boolean),
|
||||
isInvalid: false,
|
||||
invalidReason: '[REFRESH_FAILED] Token 续期失败 (401): refresh_token_reused',
|
||||
requiresReauth: true,
|
||||
usableUntilExpiry: true,
|
||||
})
|
||||
expect(status?.requiresReauth).toBe(true)
|
||||
expect(getOAuthStatusTitle({
|
||||
auth_type: 'oauth',
|
||||
oauth_expires_at: future,
|
||||
oauth_invalid_reason: '[REFRESH_FAILED] Token 续期失败 (401): refresh_token_reused',
|
||||
}, 0)).toContain('当前 Access Token 未到期仍可使用')
|
||||
expect(getOAuthRefreshButtonTitle({
|
||||
auth_type: 'oauth',
|
||||
oauth_expires_at: future,
|
||||
oauth_invalid_reason: '[REFRESH_FAILED] Token 续期失败 (401): refresh_token_reused',
|
||||
}, 0)).toBe('重新授权')
|
||||
})
|
||||
|
||||
it('shows refresh failure as reauth required while access token is still usable', () => {
|
||||
const future = Math.floor(Date.now() / 1000) + 2 * 24 * 3600
|
||||
const status = getOAuthStatusDisplay(
|
||||
{
|
||||
auth_type: 'oauth',
|
||||
oauth_expires_at: future,
|
||||
status_snapshot: {
|
||||
oauth: {
|
||||
code: 'reauth_required',
|
||||
reason: 'Token 续期失败 (400): refresh_token_reused',
|
||||
expires_at: future,
|
||||
requires_reauth: true,
|
||||
usable_until_expiry: true,
|
||||
},
|
||||
account: {
|
||||
code: 'ok',
|
||||
blocked: false,
|
||||
},
|
||||
quota: { code: 'ok', exhausted: false },
|
||||
},
|
||||
},
|
||||
0,
|
||||
)
|
||||
|
||||
expect(status).toEqual({
|
||||
text: expect.stringMatching(/^续期失败 /),
|
||||
isExpired: false,
|
||||
isExpiringSoon: expect.any(Boolean),
|
||||
isInvalid: false,
|
||||
invalidReason: 'Token 续期失败 (400): refresh_token_reused',
|
||||
requiresReauth: true,
|
||||
usableUntilExpiry: true,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ export function isAccountLevelBlockReason(reason: string | null | undefined): bo
|
||||
if (!text) return false
|
||||
if (text.startsWith('[ACCOUNT_BLOCK]')) return true
|
||||
if (text.startsWith('[OAUTH_EXPIRED]')) return true
|
||||
if (text.startsWith('[REFRESH_FAILED]')) return false
|
||||
const lowered = text.toLowerCase()
|
||||
return ACCOUNT_BLOCK_REASON_KEYWORDS.some(keyword => lowered.includes(keyword))
|
||||
}
|
||||
|
||||
@@ -84,6 +84,19 @@ function getSnapshotOAuthState(
|
||||
const expiresAt = oauth.expires_at ?? input.oauth_expires_at ?? null
|
||||
const reason = normalizeText(oauth.reason)
|
||||
|
||||
if (code === 'reauth_required') {
|
||||
const countdown = expiresAt == null ? null : getOAuthExpiresCountdown(expiresAt, tick, null, null)
|
||||
return {
|
||||
text: countdown?.text ? `续期失败 ${countdown.text}` : '续期失败',
|
||||
isExpired: false,
|
||||
isExpiringSoon: countdown?.isExpiringSoon ?? false,
|
||||
isInvalid: false,
|
||||
invalidReason: reason || undefined,
|
||||
requiresReauth: true,
|
||||
usableUntilExpiry: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (code === 'invalid') {
|
||||
return {
|
||||
text: '已失效',
|
||||
@@ -107,6 +120,27 @@ function getSnapshotOAuthState(
|
||||
return getOAuthExpiresCountdown(expiresAt, tick, null, null)
|
||||
}
|
||||
|
||||
function refreshFailureAccessTokenStillUsable(expiresAt: number | null | undefined): boolean {
|
||||
return typeof expiresAt === 'number' && expiresAt > Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
function getReauthRequiredOAuthState(
|
||||
expiresAt: number | null | undefined,
|
||||
tick: number,
|
||||
reason: string,
|
||||
): OAuthStatusInfo {
|
||||
const countdown = expiresAt == null ? null : getOAuthExpiresCountdown(expiresAt, tick, null, null)
|
||||
return {
|
||||
text: countdown?.text ? `续期失败 ${countdown.text}` : '续期失败',
|
||||
isExpired: false,
|
||||
isExpiringSoon: countdown?.isExpiringSoon ?? false,
|
||||
isInvalid: false,
|
||||
invalidReason: reason,
|
||||
requiresReauth: true,
|
||||
usableUntilExpiry: true,
|
||||
}
|
||||
}
|
||||
|
||||
function getLegacyOAuthState(
|
||||
input: ProviderKeyStatusCarrier,
|
||||
tick: number,
|
||||
@@ -115,6 +149,14 @@ function getLegacyOAuthState(
|
||||
if (!input.oauth_expires_at && !input.oauth_invalid_at && !input.oauth_invalid_reason) return null
|
||||
|
||||
const rawReason = normalizeText(input.oauth_invalid_reason)
|
||||
if (
|
||||
rawReason
|
||||
&& isRefreshFailedReason(rawReason)
|
||||
&& refreshFailureAccessTokenStillUsable(input.oauth_expires_at)
|
||||
) {
|
||||
return getReauthRequiredOAuthState(input.oauth_expires_at, tick, rawReason)
|
||||
}
|
||||
|
||||
if (rawReason && isAccountLevelBlockReason(rawReason) && !isRefreshFailedReason(rawReason)) {
|
||||
if (!input.oauth_expires_at) return null
|
||||
return getOAuthExpiresCountdown(input.oauth_expires_at, tick, null, null)
|
||||
@@ -132,6 +174,7 @@ function getOAuthStatusSeverity(status: OAuthStatusInfo | null): number {
|
||||
if (!status) return 0
|
||||
if (status.isInvalid) return 3
|
||||
if (status.isExpired) return 2
|
||||
if (status.requiresReauth) return 2
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -217,8 +260,15 @@ export function getOAuthStatusTitle(
|
||||
const reason = normalizeText(status.invalidReason)
|
||||
return reason ? `Token 已失效: ${reason}` : 'Token 已失效'
|
||||
}
|
||||
const snapshotCode = normalizeText(input.status_snapshot?.oauth?.code)
|
||||
if (snapshotCode === 'reauth_required' || status.requiresReauth) {
|
||||
const reason = normalizeText(status.invalidReason)
|
||||
return reason
|
||||
? `Refresh Token 续期失败,当前 Access Token 未到期仍可使用: ${reason}`
|
||||
: 'Refresh Token 续期失败,当前 Access Token 未到期仍可使用'
|
||||
}
|
||||
if (status.isExpired) {
|
||||
return 'Token 已过期,请重新授权'
|
||||
return 'Access Token 已过期,等待自动续期'
|
||||
}
|
||||
return `Token 剩余有效期: ${status.text}`
|
||||
}
|
||||
@@ -235,7 +285,7 @@ export function getOAuthRefreshButtonTitle(
|
||||
}
|
||||
|
||||
const status = getOAuthStatusDisplay(input, tick)
|
||||
if (status?.isInvalid || status?.isExpired) {
|
||||
if (status?.isInvalid || status?.isExpired || status?.requiresReauth) {
|
||||
return '重新授权'
|
||||
}
|
||||
return '刷新 Token'
|
||||
|
||||
Reference in New Issue
Block a user