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:
fawney19
2026-03-20 19:16:52 +08:00
parent 25d38ae632
commit 46737d32f8
39 changed files with 2326 additions and 470 deletions

View File

@@ -278,8 +278,13 @@ import {
import { exportKey, refreshProviderQuota } from '@/api/endpoints/keys'
import { refreshProviderOAuth } from '@/api/endpoints/provider_oauth'
import { useProxyNodesStore } from '@/stores/proxy-nodes'
import { classifyAccountBlockLabel, cleanAccountBlockReason, isAccountLevelBlockReason, isRefreshFailedReason } from '@/utils/accountBlock'
import { getOAuthOrgBadge } from '@/utils/oauthIdentity'
import {
getAccountStatusDisplay,
getAccountStatusTitle,
getOAuthStatusDisplay,
getOAuthStatusTitle,
} from '@/utils/providerKeyStatus'
type QuickSelectorValue =
| 'banned'
@@ -423,21 +428,12 @@ function normalizeAuthTypeLabel(authType: string): string {
}
function getStatusBadgeLabel(key: PoolKeyDetail): string | null {
const explicitLabel = String(key.account_status_label || '').trim()
if (explicitLabel) return explicitLabel
const account = getAccountStatusDisplay(key)
if (account.blocked && account.label) return account.label
const reason = String(key.oauth_invalid_reason || '').trim()
if (isAccountLevelBlockReason(reason)) {
const cleaned = cleanAccountBlockReason(reason)
return classifyAccountBlockLabel(cleaned || reason)
}
if (normalizeText(key.auth_type) !== 'oauth') return null
if (isRefreshFailedReason(reason)) return '续期失败'
if (key.oauth_invalid_at != null || normalizeText(reason)) return 'Token 失效'
if (typeof key.oauth_expires_at === 'number' && key.oauth_expires_at > 0) {
return key.oauth_expires_at * 1000 <= Date.now() ? 'Token 过期' : null
}
const oauth = getOAuthStatusDisplay(key, 0)
if (oauth?.isInvalid) return 'Token 失效'
if (oauth?.isExpired) return 'Token 过期'
return null
}
@@ -445,20 +441,11 @@ function getStatusBadgeTitle(key: PoolKeyDetail): string {
const label = getStatusBadgeLabel(key)
if (!label) return ''
const explicitReason = String(key.account_status_reason || '').trim()
if (explicitReason) return `${label}: ${explicitReason}`
const accountTitle = getAccountStatusTitle(key)
if (accountTitle) return accountTitle
const reason = String(key.oauth_invalid_reason || '').trim()
if (!reason) return label
if (isAccountLevelBlockReason(reason)) {
const cleaned = cleanAccountBlockReason(reason)
return cleaned ? `${label}: ${cleaned}` : label
}
if (isRefreshFailedReason(reason)) {
const cleaned = reason.replace(/^\[REFRESH_FAILED\]\s*/i, '').trim()
return cleaned ? `${label}: ${cleaned}` : label
}
return `${label}: ${reason}`
const oauthTitle = getOAuthStatusTitle(key, 0)
return oauthTitle || label
}
function formatRelativeTime(value: string): string {