fix(pool): 修复 Codex 配额倒计时不准确 (#297)

* fix(pool): 修复 Codex 配额倒计时不准确

- 后端号池 keys payload 增加 upstream_metadata 透传
- 前端补充 PoolKeyDetail.upstream_metadata 与 Codex reset_after_seconds 类型
- 号池页倒计时优先使用 reset_at/reset_seconds/updated_at 结构化数据计算
- 仅在缺少结构化字段时回退 account_quota 文案解析

* fix(pool): 将已重置的 Codex 配额恢复为 100%

- reset_after_seconds/reset_seconds 会按 updated_at 扣减经过时间(不再当作静态值)
- 若窗口已重置(剩余秒数 <= 0),该窗口 used_percent 按 0 处理
This commit is contained in:
AAEE86
2026-04-16 13:34:41 +08:00
committed by GitHub
parent 9a41b2c0dc
commit af9711c2a2
5 changed files with 199 additions and 35 deletions

View File

@@ -1,6 +1,11 @@
import client from '../client'
import { dedupedRequest } from '@/utils/cache'
import type { AllowedModels, OAuthOrganizationInfo, ProxyConfig } from './types/provider'
import type {
AllowedModels,
OAuthOrganizationInfo,
ProxyConfig,
UpstreamMetadata,
} from './types/provider'
import type { ProviderKeyStatusSnapshot } from './types/statusSnapshot'
const POOL_BATCH_ACTION_TIMEOUT_MS = 5 * 60 * 1000
@@ -114,6 +119,7 @@ export interface PoolKeyDetail {
account_status_recoverable?: boolean // 兼容字段;优先使用 status_snapshot.account
account_status_source?: string | null // 兼容字段;优先使用 status_snapshot.account
status_snapshot?: ProviderKeyStatusSnapshot | null
upstream_metadata?: UpstreamMetadata | null
quota_updated_at?: number | null
health_score?: number
circuit_breaker_open?: boolean

View File

@@ -307,10 +307,12 @@ export interface CodexUpstreamMetadata {
plan_type?: string // 套餐类型
primary_used_percent?: number // 周限额窗口使用百分比
primary_reset_seconds?: number // 周限额重置剩余秒数
primary_reset_after_seconds?: number // 周限额重置剩余秒数(兼容字段)
primary_reset_at?: number // 周限额重置时间Unix 时间戳)
primary_window_minutes?: number // 周限额窗口大小(分钟)
secondary_used_percent?: number // 5H限额窗口使用百分比
secondary_reset_seconds?: number // 5H限额重置剩余秒数
secondary_reset_after_seconds?: number // 5H限额重置剩余秒数兼容字段
secondary_reset_at?: number // 5H限额重置时间Unix 时间戳)
secondary_window_minutes?: number // 5H限额窗口大小分钟
has_credits?: boolean // 是否有积分

View File

@@ -1631,12 +1631,14 @@ interface QuotaProgressItem {
remainingPercent: number
detail?: string
resetAtSeconds?: number | null
resetSeconds?: number | null
updatedAtSeconds?: number | null
}
const quotaProgressMap = computed<Record<string, QuotaProgressItem[]>>(() => {
const map: Record<string, QuotaProgressItem[]> = {}
for (const key of keyPage.value.keys) {
map[key.key_id] = parseQuotaProgressItems(key.account_quota)
map[key.key_id] = parseQuotaProgressItems(key)
}
return map
})
@@ -2602,8 +2604,15 @@ function getQuotaProgressLabel(label: string): string {
}
function getQuotaProgressCountdown(item: QuotaProgressItem) {
if ((item.label !== '5H' && item.label !== '周') || item.resetAtSeconds == null) return null
return getCodexResetCountdown(item.resetAtSeconds, null, null, countdownTick.value, item.remainingPercent)
if (item.label !== '5H' && item.label !== '周') return null
if (item.resetAtSeconds == null && item.resetSeconds == null) return null
return getCodexResetCountdown(
item.resetAtSeconds,
item.resetSeconds,
item.updatedAtSeconds,
countdownTick.value,
item.remainingPercent
)
}
function getQuotaProgressCountdownText(item: QuotaProgressItem): string {
@@ -2644,6 +2653,42 @@ function clampPercent(value: number): number {
return value
}
function normalizeUnixSeconds(raw: number | null | undefined): number | null {
const value = Number(raw ?? 0)
if (!Number.isFinite(value) || value <= 0) return null
if (value > 1_000_000_000_000) return Math.floor(value / 1000)
return Math.floor(value)
}
function normalizeRemainingSeconds(raw: number | null | undefined): number | null {
const value = Number(raw ?? NaN)
if (!Number.isFinite(value) || value < 0) return null
return Math.floor(value)
}
function resolveCodexQuotaCountdown(
key: PoolKeyDetail,
label: string
): Pick<QuotaProgressItem, 'resetAtSeconds' | 'resetSeconds' | 'updatedAtSeconds'> | null {
if (label !== '5H' && label !== '周') return null
const codex = key.upstream_metadata?.codex
if (!codex) return null
const isWeeklyWindow = label === '周'
const resetAtSeconds = normalizeUnixSeconds(
isWeeklyWindow ? codex.primary_reset_at : codex.secondary_reset_at
)
const resetSeconds = normalizeRemainingSeconds(
isWeeklyWindow
? (codex.primary_reset_seconds ?? codex.primary_reset_after_seconds ?? null)
: (codex.secondary_reset_seconds ?? codex.secondary_reset_after_seconds ?? null)
)
const updatedAtSeconds = normalizeUnixSeconds(codex.updated_at)
if (resetAtSeconds == null && resetSeconds == null) return null
return { resetAtSeconds, resetSeconds, updatedAtSeconds }
}
function parseQuotaResetRemainingSeconds(detail: string | undefined): number | null {
if (!detail) return null
const text = detail.replace(/\s+/g, '')
@@ -2667,7 +2712,8 @@ function parseQuotaResetRemainingSeconds(detail: string | undefined): number | n
return total
}
function parseQuotaProgressItems(quotaText: string | null | undefined): QuotaProgressItem[] {
function parseQuotaProgressItems(key: PoolKeyDetail): QuotaProgressItem[] {
const quotaText = key.account_quota
if (!quotaText) return []
const segments = quotaText
@@ -2684,16 +2730,27 @@ function parseQuotaProgressItems(quotaText: string | null | undefined): QuotaPro
const remainingPercent = clampPercent(Number(rawPercent))
const label = normalizeQuotaLabel(rawLabel)
const detail = rawTail.trim().replace(/^[()]+|[()]+$/g, '').trim()
const resetRemainingSeconds = parseQuotaResetRemainingSeconds(detail || undefined)
const resetAtSeconds = resetRemainingSeconds == null
? null
: Math.floor(Date.now() / 1000) + resetRemainingSeconds
const codexCountdown = resolveCodexQuotaCountdown(key, label)
let resetAtSeconds = codexCountdown?.resetAtSeconds ?? null
let resetSeconds = codexCountdown?.resetSeconds ?? null
let updatedAtSeconds = codexCountdown?.updatedAtSeconds ?? null
if (resetAtSeconds == null && resetSeconds == null) {
const resetRemainingSeconds = parseQuotaResetRemainingSeconds(detail || undefined)
resetAtSeconds = resetRemainingSeconds == null
? null
: Math.floor(Date.now() / 1000) + resetRemainingSeconds
resetSeconds = null
updatedAtSeconds = null
}
items.push({
label,
remainingPercent,
detail: detail || undefined,
resetAtSeconds,
resetSeconds,
updatedAtSeconds,
})
}