fix(codex): support dynamic quota windows

This commit is contained in:
elky
2026-07-17 20:18:04 +08:00
parent 664c063a06
commit f65ed2795c
22 changed files with 1234 additions and 365 deletions
+40 -158
View File
@@ -110,21 +110,7 @@
class="px-2 font-semibold text-center whitespace-nowrap"
:style="{ width: desktopColumnWidths.stats }"
>
<div class="flex items-center justify-center gap-1.5">
<button
v-if="showCodexStatsModeToggle"
type="button"
class="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
:title="poolStatsMode === 'current_cycle' ? '切换为总计统计' : '切换为周期统计'"
:aria-label="poolStatsMode === 'current_cycle' ? '切换为总计统计' : '切换为周期统计'"
:aria-pressed="poolStatsMode === 'current_cycle'"
data-testid="pool-stats-mode-control"
@click.stop="togglePoolStatsMode"
>
<Repeat2 class="h-3.5 w-3.5" />
</button>
<span>统计</span>
</div>
<span>统计</span>
</TableHead>
<SortableTableHead
class="font-semibold text-center whitespace-nowrap"
@@ -314,7 +300,7 @@
<TableCell class="py-3 px-2 align-middle">
<PoolKeyStatsPanel
:cycle="isPoolKeyCycleStatsDisplay(key)"
:cycle-rows="getPoolKeyCycleStatsRows(key)"
:cycle-groups="getPoolKeyCycleStatsGroups(key)"
:account-metrics="getPoolKeyAccountStatsMetrics(key)"
/>
</TableCell>
@@ -592,7 +578,7 @@
<div class="space-y-1 text-center">
<PoolKeyStatsPanel
:cycle="isPoolKeyCycleStatsDisplay(key)"
:cycle-rows="getPoolKeyCycleStatsRows(key)"
:cycle-groups="getPoolKeyCycleStatsGroups(key)"
:account-metrics="getPoolKeyAccountStatsMetrics(key)"
variant="mobile"
/>
@@ -977,7 +963,6 @@ import {
Copy,
Shield,
Globe,
Repeat2,
RotateCcw,
SquarePen,
Trash2,
@@ -1065,7 +1050,6 @@ import {
resolvePoolManagementPageAfterLoad,
type PoolManagementSortBy,
type PoolManagementSortOrder,
type PoolManagementStatsMode,
type PoolManagementViewState,
writePoolManagementViewState,
} from '@/features/pool/utils/poolManagementState'
@@ -1075,7 +1059,9 @@ import {
type PoolStatsDisplay,
type PoolStatsMetric,
} from '@/features/pool/utils/poolStatsDisplay'
import { getCodexQuotaWindowPresentation } from '@/utils/codexQuotaWindow'
import { getOAuthOrgBadge } from '@/utils/oauthIdentity'
import { formatOAuthPlanType, getOAuthPlanTypeClass } from '@/utils/oauthPlanType'
import { getOAuthRefreshFeedback } from '@/utils/oauthRefreshFeedback'
import {
canEditOAuthCredential,
@@ -1119,7 +1105,6 @@ const restoredViewState = readPoolManagementViewState(
pageSize: getQueryValue('pageSize'),
sortBy: getQueryValue('sortBy'),
sortOrder: getQueryValue('sortOrder'),
statsMode: getQueryValue('statsMode'),
},
poolManagementViewStorage,
)
@@ -1487,8 +1472,6 @@ const selectedProviderType = computed(() => {
return String(fromOverview || '').trim().toLowerCase()
})
const showCodexStatsModeToggle = computed(() => selectedProviderType.value === 'codex')
const selectedProviderStatusText = computed(() => {
if (!selectedProviderId.value) return ''
const providerActive = selectedProviderData.value?.is_active
@@ -1673,7 +1656,6 @@ const currentPage = ref(restoredViewState.page)
const pageSize = ref(restoredViewState.pageSize)
const sortBy = ref<PoolManagementSortBy | null>(restoredViewState.sortBy)
const sortOrder = ref<PoolManagementSortOrder>(restoredViewState.sortOrder)
const poolStatsMode = ref<PoolManagementStatsMode>(restoredViewState.statsMode)
const hasPoolKeyFilters = computed(() => searchQuery.value.trim().length > 0 || statusFilter.value !== 'all')
const MANUAL_QUOTA_REFRESH_COOLDOWN_SECONDS = 5 * 60
const refreshingOAuthKeyId = ref<string | null>(null)
@@ -1694,12 +1676,6 @@ const keyFormDialogOpen = ref(false)
const oauthKeyEditDialogOpen = ref(false)
const editingKeyDetail = ref<PoolKeyDetail | null>(null)
function togglePoolStatsMode() {
poolStatsMode.value = poolStatsMode.value === 'current_cycle'
? 'account_total'
: 'current_cycle'
}
function clearPoolKeyFilters() {
if (!hasPoolKeyFilters.value) return
suppressFiltersWatch = true
@@ -1764,18 +1740,6 @@ watch(
{ immediate: true },
)
watch(
() => readPoolManagementViewState(
{ statsMode: getQueryValue('statsMode') },
poolManagementViewStorage,
).statsMode,
(value) => {
if (poolStatsMode.value === value) return
poolStatsMode.value = value
},
{ immediate: true },
)
watch(
() => getQueryValue('providerId'),
(value) => {
@@ -1792,8 +1756,8 @@ watch(
)
watch(
[selectedProviderId, searchQuery, statusFilter, currentPage, pageSize, sortBy, sortOrder, poolStatsMode],
([providerId, search, status, page, pageSizeValue, sortByValue, sortOrderValue, statsMode]) => {
[selectedProviderId, searchQuery, statusFilter, currentPage, pageSize, sortBy, sortOrder],
([providerId, search, status, page, pageSizeValue, sortByValue, sortOrderValue]) => {
const nextState: PoolManagementViewState = {
providerId,
search,
@@ -1802,7 +1766,7 @@ watch(
pageSize: pageSizeValue,
sortBy: sortByValue,
sortOrder: sortOrderValue,
statsMode: statsMode as PoolManagementStatsMode,
statsMode: 'current_cycle',
}
patchQuery(buildPoolManagementQueryPatch(nextState))
writePoolManagementViewState(nextState, poolManagementViewStorage)
@@ -1812,6 +1776,7 @@ watch(
interface QuotaProgressItem {
label: string
remainingPercent: number
sortOrder?: number
detail?: string
resetAtSeconds?: number | null
resetSeconds?: number | null
@@ -1828,20 +1793,6 @@ interface QuotaProgressDisplayItem {
meterClass: string
}
interface PoolCodexCycleStatsRow {
key: PoolStatsMetric['key']
label: string
fiveH: PoolStatsMetric
weekly: PoolStatsMetric
}
const CODEX_CYCLE_STAT_KEYS: Array<PoolStatsMetric['key']> = ['request_count', 'total_tokens', 'total_cost_usd']
const CODEX_CYCLE_STAT_LABELS: Record<PoolStatsMetric['key'], string> = {
request_count: '请求',
total_tokens: 'Token',
total_cost_usd: '费用',
}
type PoolKeyUiState = {
rowClass: string
schedulingBadgeLabel: string
@@ -1920,7 +1871,7 @@ const keyUiStateMap = computed<Record<string, PoolKeyUiState>>(() => {
: '',
importedAtRelative: formatPoolKeyImportedAt(key),
lastUsedRelative: key.last_used_at ? formatRelativeTime(key.last_used_at) : '-',
statsDisplay: buildPoolStatsDisplay(key, selectedProviderType.value, poolStatsMode.value),
statsDisplay: buildPoolStatsDisplay(key, selectedProviderType.value, 'current_cycle'),
mobileTagItems: getMobileTagItems(key),
mobileActionIds: splitPoolMobileActions({
canDownloadOrCopy: true,
@@ -1937,7 +1888,7 @@ const keyUiStateMap = computed<Record<string, PoolKeyUiState>>(() => {
function getPoolKeyStatsDisplay(key: PoolKeyDetail): PoolStatsDisplay {
return keyUiStateMap.value[key.key_id]?.statsDisplay
?? buildPoolStatsDisplay(key, selectedProviderType.value, poolStatsMode.value)
?? buildPoolStatsDisplay(key, selectedProviderType.value, 'current_cycle')
}
function isPoolKeyCycleStatsDisplay(key: PoolKeyDetail): boolean {
@@ -1949,39 +1900,6 @@ function getPoolKeyCycleStatsGroups(key: PoolKeyDetail): PoolCodexCycleStatsGrou
return display.kind === 'codex_cycle' ? display.groups : []
}
function createMissingCycleMetric(key: PoolStatsMetric['key']): PoolStatsMetric {
return {
key,
label: CODEX_CYCLE_STAT_LABELS[key],
value: '—',
missing: true,
}
}
function findCycleMetric(
group: PoolCodexCycleStatsGroup | undefined,
key: PoolStatsMetric['key'],
): PoolStatsMetric {
return group?.metrics.find(metric => metric.key === key) ?? createMissingCycleMetric(key)
}
function getPoolKeyCycleStatsRows(key: PoolKeyDetail): PoolCodexCycleStatsRow[] {
const groups = getPoolKeyCycleStatsGroups(key)
const fiveHGroup = groups.find(group => group.code === '5h')
const weeklyGroup = groups.find(group => group.code === 'weekly')
return CODEX_CYCLE_STAT_KEYS.map((metricKey) => {
const fiveH = findCycleMetric(fiveHGroup, metricKey)
const weekly = findCycleMetric(weeklyGroup, metricKey)
return {
key: metricKey,
label: CODEX_CYCLE_STAT_LABELS[metricKey],
fiveH,
weekly,
}
})
}
function getPoolKeyAccountStatsMetrics(key: PoolKeyDetail): PoolStatsMetric[] {
const display = getPoolKeyStatsDisplay(key)
return display.kind === 'account_total'
@@ -3110,42 +3028,6 @@ function getMobileTagClass(item: PoolMobileTagItem): string {
return 'border-border/60 bg-background/80 text-foreground/80'
}
function formatOAuthPlanType(planType: string): string {
const labelMap: Record<string, string> = {
plus: 'Plus',
pro: 'Pro',
free: 'Free',
paid: 'Paid',
team: 'Team',
enterprise: 'Enterprise',
ultra: 'Ultra',
'pro+': 'Pro+',
power: 'Power',
basic: 'Basic',
super: 'Super',
heavy: 'Heavy',
}
return labelMap[planType.toLowerCase()] || planType
}
function getOAuthPlanTypeClass(planType: string): string {
const classes: Record<string, string> = {
plus: 'border-green-500/50 text-green-600 dark:text-green-400',
pro: 'border-blue-500/50 text-blue-600 dark:text-blue-400',
free: 'border-primary/50 text-primary',
paid: 'border-blue-500/50 text-blue-600 dark:text-blue-400',
team: 'border-purple-500/50 text-purple-600 dark:text-purple-400',
enterprise: 'border-amber-500/50 text-amber-600 dark:text-amber-400',
ultra: 'border-amber-500/50 text-amber-600 dark:text-amber-400',
'pro+': 'border-purple-500/50 text-purple-600 dark:text-purple-400',
power: 'border-amber-500/50 text-amber-600 dark:text-amber-400',
basic: 'border-primary/50 text-primary',
super: 'border-green-500/50 text-green-600 dark:text-green-400',
heavy: 'border-amber-500/50 text-amber-600 dark:text-amber-400',
}
return classes[planType.toLowerCase()] || ''
}
function getVisibleOAuthState(key: PoolKeyDetail) {
return getOAuthStatusDisplayWithFallback(key, countdownTick.value)
}
@@ -3233,6 +3115,7 @@ function getQuotaProgressLabel(label: string): string {
if (label === '日') return '日'
if (label === '5H') return '5H'
if (label === '周') return '周'
if (label === '月') return '月'
if (label === 'Spark5H') return 'Spark5H'
if (label === 'Spark周') return 'Spark周'
if (label === '最低') return '最低'
@@ -3241,7 +3124,7 @@ function getQuotaProgressLabel(label: string): string {
}
function getQuotaProgressCountdown(item: QuotaProgressItem) {
const staticResetLabels = ['日', '5H', '周', 'Spark5H', 'Spark周', 'Auto', 'Fast', 'Expert', 'Heavy', 'Grok 4.3', '生图']
const staticResetLabels = ['日', '5H', '周', '月', 'Spark5H', 'Spark周', 'Spark月', 'Auto', 'Fast', 'Expert', 'Heavy', 'Grok 4.3', '生图']
if (!item.allowDynamicReset && !staticResetLabels.includes(item.label)) return null
if (item.resetAtSeconds == null && item.resetSeconds == null) return null
return getCodexResetCountdown(
@@ -3303,15 +3186,17 @@ function getQuotaLabelOrder(label: string): number {
if (label === '日') return 5
if (label === '5H') return 6
if (label === '周') return 7
if (label === 'Spark5H') return 8
if (label === 'Spark周') return 9
if (label === 'Prompt') return 10
if (label === 'Flex') return 11
if (label === '剩余') return 12
if (label === '最低') return 13
if (label === '生图') return 14
if (label === '速率') return 15
if (label === '模型') return 16
if (label === '月') return 8
if (label === 'Spark5H') return 9
if (label === 'Spark周') return 10
if (label === 'Spark月') return 11
if (label === 'Prompt') return 12
if (label === 'Flex') return 13
if (label === '剩余') return 14
if (label === '最低') return 15
if (label === '生图') return 16
if (label === '速率') return 17
if (label === '模型') return 18
return 20
}
@@ -3479,27 +3364,24 @@ function buildQuotaProgressItemsFromSnapshot(key: PoolKeyDetail): QuotaProgressI
const providerType = getQuotaSnapshotProviderType(key)
if (providerType === 'codex') {
const items: QuotaProgressItem[] = []
const quotaResetAtSeconds = getQuotaSnapshotResetAtSeconds(quota)
const quotaResetSeconds = getQuotaSnapshotResetSeconds(quota)
for (const [label, code] of [
['5H', '5h'],
['周', 'weekly'],
['Spark5H', 'spark_5h'],
['Spark周', 'spark_weekly'],
] as const) {
const window = getQuotaSnapshotWindow(quota, code)
const remainingPercent = getQuotaWindowRemainingPercent(window)
if (remainingPercent == null) continue
items.push({
label,
remainingPercent,
resetAtSeconds: normalizeUnixSeconds(window?.reset_at ?? quotaResetAtSeconds ?? null),
resetSeconds: normalizeRemainingSeconds(window?.reset_seconds ?? quotaResetSeconds ?? null),
updatedAtSeconds: getQuotaSnapshotUpdatedAtSeconds(quota),
return (quota.windows ?? [])
.map((window): QuotaProgressItem | null => {
const presentation = getCodexQuotaWindowPresentation(window)
const remainingPercent = getQuotaWindowRemainingPercent(window)
if (!presentation || remainingPercent == null) return null
return {
label: presentation.label,
sortOrder: presentation.sortOrder,
remainingPercent,
resetAtSeconds: normalizeUnixSeconds(window.reset_at ?? quotaResetAtSeconds ?? null),
resetSeconds: normalizeRemainingSeconds(window.reset_seconds ?? quotaResetSeconds ?? null),
updatedAtSeconds: getQuotaSnapshotUpdatedAtSeconds(quota),
allowDynamicReset: true,
}
})
}
return items
.filter((item): item is QuotaProgressItem => item != null)
}
if (providerType === 'kiro') {
@@ -3739,7 +3621,7 @@ function parseQuotaProgressItems(key: PoolKeyDetail): QuotaProgressItem[] {
const snapshotItems = buildQuotaProgressItemsFromSnapshot(key)
if (snapshotItems.length > 0) {
return snapshotItems.sort((a, b) => {
const orderDiff = getQuotaLabelOrder(a.label) - getQuotaLabelOrder(b.label)
const orderDiff = (a.sortOrder ?? getQuotaLabelOrder(a.label)) - (b.sortOrder ?? getQuotaLabelOrder(b.label))
if (orderDiff !== 0) return orderDiff
return a.label.localeCompare(b.label, 'zh-Hans-CN')
})