mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat: 引入 status_snapshot 统一 provider key 状态管理
- 新增 StatusSnapshot 模型,聚合 OAuth / 账号 / 配额三维状态 - 新增 StatusSnapshotStore 负责快照的持久化与查询 - 重构 response_builder / endpoint_models,基于 snapshot 输出状态字段 - 前端抽取 providerKeyStatus / oauthRefreshFeedback 工具函数, 统一 PoolManagement、ProviderDetailDrawer、BatchDialog 的状态展示 - errorParser 增加已知 OAuth 错误的友好提示 - refresher 适配 snapshot 写入,account_state 扩展状态分类 - 新增 alembic 迁移及存量数据回填脚本 - 补充前后端单元测试
This commit is contained in:
21
frontend/src/utils/__tests__/accountBlock.spec.ts
Normal file
21
frontend/src/utils/__tests__/accountBlock.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { cleanAccountBlockReason, isRefreshFailedReason } from '@/utils/accountBlock'
|
||||
|
||||
describe('accountBlock helpers', () => {
|
||||
it('detects refresh failure markers even when account block is also present', () => {
|
||||
expect(
|
||||
isRefreshFailedReason(
|
||||
'[ACCOUNT_BLOCK] 工作区已停用 (deactivated_workspace)\n[REFRESH_FAILED] Token 续期失败',
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps account block reason clean when refresh failure is appended', () => {
|
||||
expect(
|
||||
cleanAccountBlockReason(
|
||||
'[ACCOUNT_BLOCK] 工作区已停用 (deactivated_workspace)\n[REFRESH_FAILED] Token 续期失败',
|
||||
),
|
||||
).toBe('工作区已停用 (deactivated_workspace)')
|
||||
})
|
||||
})
|
||||
33
frontend/src/utils/__tests__/errorParser.spec.ts
Normal file
33
frontend/src/utils/__tests__/errorParser.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
|
||||
describe('errorParser', () => {
|
||||
it('normalizes reused refresh token errors from legacy string details', () => {
|
||||
const error = {
|
||||
response: {
|
||||
data: {
|
||||
detail: "token refresh 失败: {'message': 'Your refresh token has already been used to generate a new access token. Please try signing in again.', 'type': 'invalid_request_error', 'param': None, 'code': 'refresh_token_reused'}",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expect(parseApiError(error, 'Token 刷新失败')).toBe(
|
||||
'Token 刷新失败:refresh_token 已被使用并轮换,请重新登录授权',
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps normalized Chinese refresh failures intact', () => {
|
||||
const error = {
|
||||
response: {
|
||||
data: {
|
||||
detail: 'Token 刷新失败:refresh_token 已被使用并轮换,请重新登录授权',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expect(parseApiError(error, 'Token 刷新失败')).toBe(
|
||||
'Token 刷新失败:refresh_token 已被使用并轮换,请重新登录授权',
|
||||
)
|
||||
})
|
||||
})
|
||||
20
frontend/src/utils/__tests__/oauthCountdown.spec.ts
Normal file
20
frontend/src/utils/__tests__/oauthCountdown.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getOAuthExpiresCountdown } from '@/composables/useCountdownTimer'
|
||||
|
||||
describe('getOAuthExpiresCountdown', () => {
|
||||
it('treats invalid reason as invalid even without invalid timestamp', () => {
|
||||
expect(
|
||||
getOAuthExpiresCountdown(
|
||||
Math.floor(Date.now() / 1000) + 3600,
|
||||
0,
|
||||
null,
|
||||
'[REFRESH_FAILED] Token 续期失败 (400): refresh_token_reused',
|
||||
),
|
||||
).toMatchObject({
|
||||
text: '已失效',
|
||||
isInvalid: true,
|
||||
invalidReason: '[REFRESH_FAILED] Token 续期失败 (400): refresh_token_reused',
|
||||
})
|
||||
})
|
||||
})
|
||||
85
frontend/src/utils/__tests__/oauthRefreshFeedback.spec.ts
Normal file
85
frontend/src/utils/__tests__/oauthRefreshFeedback.spec.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
getOAuthRefreshFeedback,
|
||||
resolveOAuthAccountBlockDisplay,
|
||||
} from '@/utils/oauthRefreshFeedback'
|
||||
|
||||
describe('oauthRefreshFeedback', () => {
|
||||
it('prefers explicit blocked account status from pool data', () => {
|
||||
expect(
|
||||
resolveOAuthAccountBlockDisplay({
|
||||
status_snapshot: {
|
||||
oauth: { code: 'valid' },
|
||||
account: {
|
||||
code: 'account_disabled',
|
||||
label: '账号停用',
|
||||
reason: 'account has been deactivated',
|
||||
blocked: true,
|
||||
},
|
||||
quota: { code: 'unknown', exhausted: false },
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
label: '账号停用',
|
||||
reason: 'account has been deactivated',
|
||||
})
|
||||
})
|
||||
|
||||
it('classifies account-level OAuth invalid reasons', () => {
|
||||
expect(
|
||||
resolveOAuthAccountBlockDisplay({
|
||||
oauth_invalid_reason: '[ACCOUNT_BLOCK] account has been deactivated',
|
||||
}),
|
||||
).toEqual({
|
||||
label: '账号停用',
|
||||
reason: 'account has been deactivated',
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores recoverable refresh failures', () => {
|
||||
expect(
|
||||
resolveOAuthAccountBlockDisplay({
|
||||
oauth_invalid_reason: '[REFRESH_FAILED] Token 续期失败',
|
||||
}),
|
||||
).toEqual({
|
||||
label: null,
|
||||
reason: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('reports blocked result after successful recheck', () => {
|
||||
expect(
|
||||
getOAuthRefreshFeedback({
|
||||
accountStateRecheckAttempted: true,
|
||||
snapshot: {
|
||||
status_snapshot: {
|
||||
oauth: { code: 'valid' },
|
||||
account: {
|
||||
code: 'account_disabled',
|
||||
label: '账号停用',
|
||||
reason: 'account has been deactivated',
|
||||
blocked: true,
|
||||
},
|
||||
quota: { code: 'unknown', exhausted: false },
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
tone: 'warning',
|
||||
message: 'Token 刷新成功,已重新检查额度/账号状态;当前状态仍是账号停用',
|
||||
})
|
||||
})
|
||||
|
||||
it('reports plain success when no blocked state remains', () => {
|
||||
expect(
|
||||
getOAuthRefreshFeedback({
|
||||
accountStateRecheckAttempted: true,
|
||||
accountStateRecheckError: null,
|
||||
}),
|
||||
).toEqual({
|
||||
tone: 'success',
|
||||
message: 'Token 刷新成功,已重新检查额度/账号状态',
|
||||
})
|
||||
})
|
||||
})
|
||||
107
frontend/src/utils/__tests__/providerKeyStatus.spec.ts
Normal file
107
frontend/src/utils/__tests__/providerKeyStatus.spec.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
getAccountStatusDisplay,
|
||||
getOAuthStatusDisplay,
|
||||
getOAuthStatusTitle,
|
||||
} from '@/utils/providerKeyStatus'
|
||||
|
||||
describe('providerKeyStatus', () => {
|
||||
it('uses status_snapshot account state as the primary source', () => {
|
||||
expect(
|
||||
getAccountStatusDisplay({
|
||||
status_snapshot: {
|
||||
oauth: { code: 'valid' },
|
||||
account: {
|
||||
code: 'workspace_deactivated',
|
||||
label: '工作区停用',
|
||||
reason: 'deactivated_workspace',
|
||||
blocked: true,
|
||||
},
|
||||
quota: { code: 'ok', exhausted: false },
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
code: 'workspace_deactivated',
|
||||
label: '工作区停用',
|
||||
reason: 'deactivated_workspace',
|
||||
blocked: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('shows oauth invalid when refresh failure exists beside account block', () => {
|
||||
const status = getOAuthStatusDisplay(
|
||||
{
|
||||
auth_type: 'oauth',
|
||||
oauth_expires_at: 2_000_000_000,
|
||||
status_snapshot: {
|
||||
oauth: {
|
||||
code: 'invalid',
|
||||
reason: 'Token 续期失败 (400): refresh_token_reused',
|
||||
expires_at: 2_000_000_000,
|
||||
requires_reauth: true,
|
||||
},
|
||||
account: {
|
||||
code: 'workspace_deactivated',
|
||||
label: '工作区停用',
|
||||
reason: 'deactivated_workspace',
|
||||
blocked: true,
|
||||
},
|
||||
quota: { code: 'ok', exhausted: false },
|
||||
},
|
||||
},
|
||||
0,
|
||||
)
|
||||
|
||||
expect(status).toEqual({
|
||||
text: '已失效',
|
||||
isExpired: false,
|
||||
isExpiringSoon: false,
|
||||
isInvalid: true,
|
||||
invalidReason: 'Token 续期失败 (400): refresh_token_reused',
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to countdown for account block without oauth invalidation', () => {
|
||||
const status = getOAuthStatusDisplay(
|
||||
{
|
||||
auth_type: 'oauth',
|
||||
oauth_expires_at: Math.floor(Date.now() / 1000) + 3 * 24 * 3600,
|
||||
status_snapshot: {
|
||||
oauth: {
|
||||
code: 'valid',
|
||||
expires_at: Math.floor(Date.now() / 1000) + 3 * 24 * 3600,
|
||||
},
|
||||
account: {
|
||||
code: 'account_disabled',
|
||||
label: '账号停用',
|
||||
reason: 'account has been deactivated',
|
||||
blocked: true,
|
||||
},
|
||||
quota: { code: 'ok', exhausted: false },
|
||||
},
|
||||
},
|
||||
0,
|
||||
)
|
||||
|
||||
expect(status?.isInvalid).toBe(false)
|
||||
expect(status?.isExpired).toBe(false)
|
||||
expect(getOAuthStatusTitle({
|
||||
auth_type: 'oauth',
|
||||
oauth_expires_at: Math.floor(Date.now() / 1000) + 3 * 24 * 3600,
|
||||
status_snapshot: {
|
||||
oauth: {
|
||||
code: 'valid',
|
||||
expires_at: Math.floor(Date.now() / 1000) + 3 * 24 * 3600,
|
||||
},
|
||||
account: {
|
||||
code: 'account_disabled',
|
||||
label: '账号停用',
|
||||
reason: 'account has been deactivated',
|
||||
blocked: true,
|
||||
},
|
||||
quota: { code: 'ok', exhausted: false },
|
||||
},
|
||||
}, 0)).toContain('Token 剩余有效期:')
|
||||
})
|
||||
})
|
||||
@@ -72,12 +72,15 @@ export function classifyAccountBlockLabel(reason: string): string {
|
||||
}
|
||||
|
||||
export function cleanAccountBlockReason(reason: string): string {
|
||||
return reason.replace(/^\[(ACCOUNT_BLOCK|OAUTH_EXPIRED)\]\s*/i, '').trim()
|
||||
return reason
|
||||
.replace(/^\[(ACCOUNT_BLOCK|OAUTH_EXPIRED)\]\s*/i, '')
|
||||
.replace(/\s*\[REFRESH_FAILED\][\s\S]*$/i, '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
export function isRefreshFailedReason(reason: string | null | undefined): boolean {
|
||||
if (!reason) return false
|
||||
return reason.trim().startsWith('[REFRESH_FAILED]')
|
||||
return reason.includes('[REFRESH_FAILED]')
|
||||
}
|
||||
|
||||
export function isOAuthExpiredReason(reason: string | null | undefined): boolean {
|
||||
|
||||
@@ -133,6 +133,30 @@ function formatValidationError(error: ValidationError): string {
|
||||
return `${fieldName}: ${error.msg}`
|
||||
}
|
||||
|
||||
function normalizeKnownApiErrorMessage(message: string): string {
|
||||
const text = message.trim()
|
||||
if (!text) return text
|
||||
|
||||
const lowered = text.toLowerCase()
|
||||
if (
|
||||
lowered.includes('refresh_token_reused')
|
||||
|| lowered.includes('already been used to generate a new access token')
|
||||
) {
|
||||
return 'Token 刷新失败:refresh_token 已被使用并轮换,请重新登录授权'
|
||||
}
|
||||
|
||||
if (
|
||||
lowered.includes('token refresh 失败:')
|
||||
|| lowered.includes('token refresh failed:')
|
||||
) {
|
||||
return text
|
||||
.replace(/^token refresh 失败:\s*/i, 'Token 刷新失败:')
|
||||
.replace(/^token refresh failed:\s*/i, 'Token 刷新失败:')
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 API 错误响应
|
||||
* @param err 错误对象
|
||||
@@ -145,7 +169,7 @@ export function parseApiError(err: unknown, defaultMessage: string = '操作失
|
||||
// 处理网络错误
|
||||
if (!isApiError(err) || !err.response) {
|
||||
if (err instanceof Error) {
|
||||
return err.message || defaultMessage
|
||||
return normalizeKnownApiErrorMessage(err.message || defaultMessage)
|
||||
}
|
||||
return '无法连接到服务器,请检查网络连接'
|
||||
}
|
||||
@@ -154,14 +178,14 @@ export function parseApiError(err: unknown, defaultMessage: string = '操作失
|
||||
|
||||
// 1. 处理 {error: {type, message}} 格式(ProxyException 返回格式)
|
||||
if (data?.error?.message) {
|
||||
return data.error.message
|
||||
return normalizeKnownApiErrorMessage(data.error.message)
|
||||
}
|
||||
|
||||
const detail = data?.detail
|
||||
|
||||
// 如果没有 detail 字段
|
||||
if (!detail) {
|
||||
return data?.message || err.message || defaultMessage
|
||||
return normalizeKnownApiErrorMessage(data?.message || err.message || defaultMessage)
|
||||
}
|
||||
|
||||
// 1. 处理 Pydantic 验证错误(数组格式)
|
||||
@@ -174,14 +198,14 @@ export function parseApiError(err: unknown, defaultMessage: string = '操作失
|
||||
|
||||
// 2. 处理字符串错误
|
||||
if (typeof detail === 'string') {
|
||||
return detail
|
||||
return normalizeKnownApiErrorMessage(detail)
|
||||
}
|
||||
|
||||
// 3. 处理对象错误
|
||||
if (typeof detail === 'object') {
|
||||
// 可能是自定义错误对象
|
||||
if ((detail as Record<string, unknown>).message) {
|
||||
return String((detail as Record<string, unknown>).message)
|
||||
return normalizeKnownApiErrorMessage(String((detail as Record<string, unknown>).message))
|
||||
}
|
||||
// 尝试 JSON 序列化
|
||||
try {
|
||||
|
||||
74
frontend/src/utils/oauthRefreshFeedback.ts
Normal file
74
frontend/src/utils/oauthRefreshFeedback.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
getAccountStatusDisplay,
|
||||
type ProviderKeyStatusCarrier,
|
||||
} from './providerKeyStatus'
|
||||
|
||||
export interface OAuthAccountBlockDisplay {
|
||||
label: string | null
|
||||
reason: string | null
|
||||
}
|
||||
|
||||
export interface OAuthRefreshFeedbackInput {
|
||||
accountStateRecheckAttempted?: boolean | null
|
||||
accountStateRecheckError?: string | null
|
||||
snapshot?: ProviderKeyStatusCarrier | null
|
||||
}
|
||||
|
||||
function normalizeText(value: unknown): string | null {
|
||||
if (typeof value !== 'string') return null
|
||||
const text = value.trim()
|
||||
return text || null
|
||||
}
|
||||
|
||||
export function resolveOAuthAccountBlockDisplay(
|
||||
snapshot: ProviderKeyStatusCarrier,
|
||||
): OAuthAccountBlockDisplay {
|
||||
const account = getAccountStatusDisplay(snapshot)
|
||||
if (!account.blocked || !account.label) {
|
||||
return { label: null, reason: null }
|
||||
}
|
||||
return {
|
||||
label: account.label,
|
||||
reason: account.reason,
|
||||
}
|
||||
}
|
||||
|
||||
export function getOAuthRefreshFeedback(
|
||||
input: OAuthRefreshFeedbackInput,
|
||||
): { tone: 'success' | 'warning'; message: string } {
|
||||
const blockedLabel = normalizeText(
|
||||
input.snapshot ? getAccountStatusDisplay(input.snapshot).label : null,
|
||||
)
|
||||
const recheckError = normalizeText(input.accountStateRecheckError)
|
||||
|
||||
if (input.accountStateRecheckAttempted) {
|
||||
if (recheckError) {
|
||||
return {
|
||||
tone: 'warning',
|
||||
message: 'Token 刷新成功,但额度/账号状态复检失败',
|
||||
}
|
||||
}
|
||||
if (blockedLabel) {
|
||||
return {
|
||||
tone: 'warning',
|
||||
message: `Token 刷新成功,已重新检查额度/账号状态;当前状态仍是${blockedLabel}`,
|
||||
}
|
||||
}
|
||||
return {
|
||||
tone: 'success',
|
||||
message: 'Token 刷新成功,已重新检查额度/账号状态',
|
||||
}
|
||||
}
|
||||
|
||||
if (blockedLabel) {
|
||||
return {
|
||||
tone: 'warning',
|
||||
message: `Token 刷新成功,但当前状态仍是${blockedLabel}`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tone: 'success',
|
||||
message: 'Token 刷新成功',
|
||||
}
|
||||
}
|
||||
165
frontend/src/utils/providerKeyStatus.ts
Normal file
165
frontend/src/utils/providerKeyStatus.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import type { ProviderKeyStatusSnapshot } from '@/api/endpoints/types/statusSnapshot'
|
||||
import { getOAuthExpiresCountdown, type OAuthStatusInfo } from '@/composables/useCountdownTimer'
|
||||
import {
|
||||
classifyAccountBlockLabel,
|
||||
cleanAccountBlockReason,
|
||||
isAccountLevelBlockReason,
|
||||
isRefreshFailedReason,
|
||||
} from './accountBlock'
|
||||
|
||||
export interface ProviderKeyStatusCarrier {
|
||||
auth_type?: string | null
|
||||
oauth_expires_at?: number | null
|
||||
oauth_invalid_at?: number | null // compatibility only
|
||||
oauth_invalid_reason?: string | null // compatibility only
|
||||
account_status_label?: string | null // compatibility only
|
||||
account_status_reason?: string | null // compatibility only
|
||||
account_status_blocked?: boolean | null // compatibility only
|
||||
status_snapshot?: ProviderKeyStatusSnapshot | null
|
||||
}
|
||||
|
||||
export interface AccountStatusDisplay {
|
||||
code: string
|
||||
label: string | null
|
||||
reason: string | null
|
||||
blocked: boolean
|
||||
}
|
||||
|
||||
function normalizeText(value: unknown): string | null {
|
||||
if (typeof value !== 'string') return null
|
||||
const text = value.trim()
|
||||
return text || null
|
||||
}
|
||||
|
||||
function buildLegacyAccountStatus(input: ProviderKeyStatusCarrier): AccountStatusDisplay {
|
||||
const explicitLabel = normalizeText(input.account_status_label)
|
||||
if (input.account_status_blocked && explicitLabel) {
|
||||
return {
|
||||
code: 'legacy',
|
||||
label: explicitLabel,
|
||||
reason: normalizeText(input.account_status_reason),
|
||||
blocked: true,
|
||||
}
|
||||
}
|
||||
|
||||
const invalidReason = normalizeText(input.oauth_invalid_reason)
|
||||
if (!invalidReason || !isAccountLevelBlockReason(invalidReason)) {
|
||||
return { code: 'ok', label: null, reason: null, blocked: false }
|
||||
}
|
||||
|
||||
const cleaned = cleanAccountBlockReason(invalidReason) || invalidReason
|
||||
return {
|
||||
code: 'legacy',
|
||||
label: classifyAccountBlockLabel(cleaned || invalidReason),
|
||||
reason: normalizeText(cleaned),
|
||||
blocked: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function getAccountStatusDisplay(input: ProviderKeyStatusCarrier): AccountStatusDisplay {
|
||||
const snapshot = input.status_snapshot?.account
|
||||
if (snapshot) {
|
||||
return {
|
||||
code: normalizeText(snapshot.code) || 'ok',
|
||||
label: normalizeText(snapshot.label),
|
||||
reason: normalizeText(snapshot.reason),
|
||||
blocked: Boolean(snapshot.blocked),
|
||||
}
|
||||
}
|
||||
return buildLegacyAccountStatus(input)
|
||||
}
|
||||
|
||||
function getSnapshotOAuthState(
|
||||
input: ProviderKeyStatusCarrier,
|
||||
tick: number,
|
||||
): OAuthStatusInfo | null {
|
||||
const oauth = input.status_snapshot?.oauth
|
||||
if (!oauth) return null
|
||||
|
||||
const code = normalizeText(oauth.code) || 'none'
|
||||
const expiresAt = oauth.expires_at ?? input.oauth_expires_at ?? null
|
||||
const reason = normalizeText(oauth.reason)
|
||||
|
||||
if (code === 'invalid') {
|
||||
return {
|
||||
text: '已失效',
|
||||
isExpired: false,
|
||||
isExpiringSoon: false,
|
||||
isInvalid: true,
|
||||
invalidReason: reason || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
if (code === 'expired') {
|
||||
return { text: '已过期', isExpired: true, isExpiringSoon: false, isInvalid: false }
|
||||
}
|
||||
|
||||
if (code === 'check_failed') {
|
||||
if (expiresAt == null) return null
|
||||
return getOAuthExpiresCountdown(expiresAt, tick, null, null)
|
||||
}
|
||||
|
||||
if (expiresAt == null) return null
|
||||
return getOAuthExpiresCountdown(expiresAt, tick, null, null)
|
||||
}
|
||||
|
||||
function getLegacyOAuthState(
|
||||
input: ProviderKeyStatusCarrier,
|
||||
tick: number,
|
||||
): OAuthStatusInfo | null {
|
||||
if (normalizeText(input.auth_type) !== 'oauth') return null
|
||||
if (!input.oauth_expires_at && !input.oauth_invalid_at && !input.oauth_invalid_reason) return null
|
||||
|
||||
const rawReason = normalizeText(input.oauth_invalid_reason)
|
||||
if (rawReason && isAccountLevelBlockReason(rawReason) && !isRefreshFailedReason(rawReason)) {
|
||||
if (!input.oauth_expires_at) return null
|
||||
return getOAuthExpiresCountdown(input.oauth_expires_at, tick, null, null)
|
||||
}
|
||||
|
||||
return getOAuthExpiresCountdown(
|
||||
input.oauth_expires_at,
|
||||
tick,
|
||||
input.oauth_invalid_at,
|
||||
input.oauth_invalid_reason,
|
||||
)
|
||||
}
|
||||
|
||||
export function getOAuthStatusDisplay(
|
||||
input: ProviderKeyStatusCarrier,
|
||||
tick: number,
|
||||
): OAuthStatusInfo | null {
|
||||
return getSnapshotOAuthState(input, tick) ?? getLegacyOAuthState(input, tick)
|
||||
}
|
||||
|
||||
export function getOAuthStatusTitle(
|
||||
input: ProviderKeyStatusCarrier,
|
||||
tick: number,
|
||||
): string {
|
||||
const status = getOAuthStatusDisplay(input, tick)
|
||||
if (!status) return ''
|
||||
if (status.isInvalid) {
|
||||
const reason = normalizeText(status.invalidReason)
|
||||
return reason ? `Token 已失效: ${reason}` : 'Token 已失效'
|
||||
}
|
||||
if (status.isExpired) {
|
||||
return 'Token 已过期,请重新授权'
|
||||
}
|
||||
return `Token 剩余有效期: ${status.text}`
|
||||
}
|
||||
|
||||
export function getOAuthRefreshButtonTitle(
|
||||
input: ProviderKeyStatusCarrier,
|
||||
tick: number,
|
||||
): string {
|
||||
const status = getOAuthStatusDisplay(input, tick)
|
||||
if (status?.isInvalid || status?.isExpired) {
|
||||
return '重新授权'
|
||||
}
|
||||
return '刷新 Token'
|
||||
}
|
||||
|
||||
export function getAccountStatusTitle(input: ProviderKeyStatusCarrier): string {
|
||||
const account = getAccountStatusDisplay(input)
|
||||
if (!account.blocked || !account.label) return ''
|
||||
return account.reason ? `${account.label}: ${account.reason}` : account.label
|
||||
}
|
||||
Reference in New Issue
Block a user