mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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 // 是否有积分
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user