2026-07-04 06:10:53 +08:00
|
|
|
import type {
|
2026-07-16 15:21:11 +08:00
|
|
|
CodexUpstreamMetadata,
|
2026-07-04 06:10:53 +08:00
|
|
|
QuotaResetCreditSnapshot,
|
|
|
|
|
QuotaResetCreditsSnapshot,
|
|
|
|
|
} from '@/api/endpoints/types'
|
|
|
|
|
|
2026-07-16 15:21:11 +08:00
|
|
|
function codexQuotaUpdatedAt(display: CodexUpstreamMetadata | null | undefined): number | null {
|
|
|
|
|
const updatedAt = Number(display?.updated_at)
|
|
|
|
|
return Number.isFinite(updatedAt) ? updatedAt : null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function mergeCodexQuotaDisplays(
|
|
|
|
|
snapshotDisplay: CodexUpstreamMetadata | null | undefined,
|
|
|
|
|
metadataDisplay: CodexUpstreamMetadata | null | undefined,
|
|
|
|
|
): CodexUpstreamMetadata | null {
|
|
|
|
|
if (!snapshotDisplay) return metadataDisplay ?? null
|
|
|
|
|
if (!metadataDisplay) return snapshotDisplay
|
|
|
|
|
|
|
|
|
|
const snapshotUpdatedAt = codexQuotaUpdatedAt(snapshotDisplay)
|
|
|
|
|
const metadataUpdatedAt = codexQuotaUpdatedAt(metadataDisplay)
|
|
|
|
|
const metadataIsNewer = metadataUpdatedAt !== null
|
|
|
|
|
&& (snapshotUpdatedAt === null || metadataUpdatedAt > snapshotUpdatedAt)
|
|
|
|
|
const preferred = metadataIsNewer ? metadataDisplay : snapshotDisplay
|
|
|
|
|
const fallback = metadataIsNewer ? snapshotDisplay : metadataDisplay
|
|
|
|
|
const resetCredits = preferred.reset_credits || fallback.reset_credits
|
|
|
|
|
? {
|
|
|
|
|
...fallback.reset_credits,
|
|
|
|
|
...preferred.reset_credits,
|
|
|
|
|
}
|
|
|
|
|
: undefined
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...fallback,
|
|
|
|
|
...preferred,
|
|
|
|
|
...(resetCredits ? { reset_credits: resetCredits } : {}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-04 06:10:53 +08:00
|
|
|
export interface CodexResetCreditDisplayItem {
|
|
|
|
|
id?: string | null
|
|
|
|
|
displayKey: string
|
|
|
|
|
expiresAt?: number | null
|
|
|
|
|
remainingSeconds: number
|
|
|
|
|
title: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-04 15:16:38 +08:00
|
|
|
interface CodexResetCreditDisplayCandidate {
|
|
|
|
|
id?: string | null
|
|
|
|
|
expiresAt?: number | null
|
|
|
|
|
remainingSeconds: number
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-04 06:10:53 +08:00
|
|
|
export function getCodexResetCreditAvailableCount(
|
|
|
|
|
snapshot: QuotaResetCreditsSnapshot | null | undefined,
|
|
|
|
|
): number | null {
|
|
|
|
|
const count = snapshot?.available_count
|
|
|
|
|
return typeof count === 'number' && Number.isFinite(count) && count >= 0 ? count : null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function formatCodexResetCreditCount(count: number | null | undefined): string {
|
|
|
|
|
return `共 ${count ?? 0} 次机会`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function codexResetCreditRemainingSeconds(
|
|
|
|
|
item: QuotaResetCreditSnapshot,
|
|
|
|
|
snapshot: QuotaResetCreditsSnapshot,
|
|
|
|
|
nowUnixSecs: number,
|
|
|
|
|
): number | null {
|
|
|
|
|
if (typeof item.expires_at === 'number' && Number.isFinite(item.expires_at)) {
|
|
|
|
|
return Math.max(item.expires_at - nowUnixSecs, 0)
|
|
|
|
|
}
|
|
|
|
|
if (typeof item.remaining_seconds === 'number' && Number.isFinite(item.remaining_seconds)) {
|
|
|
|
|
const updatedAt = snapshot.updated_at
|
|
|
|
|
const elapsed = typeof updatedAt === 'number' && Number.isFinite(updatedAt)
|
|
|
|
|
? Math.max(nowUnixSecs - updatedAt, 0)
|
|
|
|
|
: 0
|
|
|
|
|
return Math.max(item.remaining_seconds - elapsed, 0)
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function codexResetCreditStatusIsDisplayable(item: QuotaResetCreditSnapshot): boolean {
|
|
|
|
|
const status = item.status?.trim().toLowerCase()
|
|
|
|
|
return !status || status === 'available' || status === 'active'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getVisibleCodexResetCreditItems(
|
|
|
|
|
snapshot: QuotaResetCreditsSnapshot | null | undefined,
|
|
|
|
|
nowUnixSecs = Math.floor(Date.now() / 1000),
|
|
|
|
|
limit = 5,
|
|
|
|
|
): CodexResetCreditDisplayItem[] {
|
|
|
|
|
const credits = snapshot?.credits
|
|
|
|
|
if (!snapshot || !Array.isArray(credits)) return []
|
|
|
|
|
|
|
|
|
|
return credits
|
|
|
|
|
.map((item) => {
|
|
|
|
|
if (!codexResetCreditStatusIsDisplayable(item)) return null
|
|
|
|
|
const remainingSeconds = codexResetCreditRemainingSeconds(item, snapshot, nowUnixSecs)
|
2026-07-04 15:16:38 +08:00
|
|
|
if (remainingSeconds === null || remainingSeconds <= 0) return null
|
2026-07-04 06:10:53 +08:00
|
|
|
return {
|
|
|
|
|
id: item.id,
|
|
|
|
|
expiresAt: item.expires_at,
|
|
|
|
|
remainingSeconds,
|
2026-07-04 15:16:38 +08:00
|
|
|
} satisfies CodexResetCreditDisplayCandidate
|
2026-07-04 06:10:53 +08:00
|
|
|
})
|
2026-07-04 15:16:38 +08:00
|
|
|
.filter((item): item is CodexResetCreditDisplayCandidate => item !== null)
|
2026-07-04 06:10:53 +08:00
|
|
|
.sort((a, b) => a.remainingSeconds - b.remainingSeconds)
|
|
|
|
|
.slice(0, limit)
|
2026-07-04 15:16:38 +08:00
|
|
|
.map((item, index) => {
|
|
|
|
|
const displayKey = `Key-${index + 1}`
|
|
|
|
|
return {
|
|
|
|
|
...item,
|
|
|
|
|
displayKey,
|
|
|
|
|
title: `Codex 重置机会 ${displayKey}`,
|
|
|
|
|
} satisfies CodexResetCreditDisplayItem
|
|
|
|
|
})
|
2026-07-04 06:10:53 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function formatCodexResetCreditDays(remainingSeconds: number): string {
|
|
|
|
|
const days = Math.max(1, Math.ceil(remainingSeconds / 86_400))
|
|
|
|
|
return `${days}天`
|
|
|
|
|
}
|