feat: OpenAI OAuth账号显示Codex使用量

从响应头提取x-codex-*使用量信息并保存到账号Extra字段,
前端账号列表展示5h/7d窗口的使用进度条。
This commit is contained in:
shaw
2025-12-23 16:26:07 +08:00
parent f6341b7f2b
commit f25ac3aff5
6 changed files with 245 additions and 14 deletions

View File

@@ -68,7 +68,31 @@
<SetupTokenTimeWindow :account="account" />
</template>
<!-- OpenAI accounts: no usage window API, show dash -->
<!-- OpenAI OAuth accounts: show Codex usage from extra field -->
<template v-else-if="account.platform === 'openai' && account.type === 'oauth'">
<div v-if="hasCodexUsage" class="space-y-1">
<!-- 5h Window (Secondary) -->
<UsageProgressBar
v-if="codexSecondaryUsedPercent !== null"
label="5h"
:utilization="codexSecondaryUsedPercent"
:resets-at="codexSecondaryResetAt"
color="indigo"
/>
<!-- Weekly Window (Primary) -->
<UsageProgressBar
v-if="codexPrimaryUsedPercent !== null"
label="7d"
:utilization="codexPrimaryUsedPercent"
:resets-at="codexPrimaryResetAt"
color="emerald"
/>
</div>
<div v-else class="text-xs text-gray-400">-</div>
</template>
<!-- Other accounts: no usage window -->
<template v-else>
<div class="text-xs text-gray-400">-</div>
</template>
@@ -100,9 +124,44 @@ const showUsageWindows = computed(() =>
props.account.type === 'oauth' || props.account.type === 'setup-token'
)
// OpenAI Codex usage computed properties
const hasCodexUsage = computed(() => {
const extra = props.account.extra
return extra && (
extra.codex_primary_used_percent !== undefined ||
extra.codex_secondary_used_percent !== undefined
)
})
const codexPrimaryUsedPercent = computed(() => {
const extra = props.account.extra
if (!extra || extra.codex_primary_used_percent === undefined) return null
return extra.codex_primary_used_percent
})
const codexSecondaryUsedPercent = computed(() => {
const extra = props.account.extra
if (!extra || extra.codex_secondary_used_percent === undefined) return null
return extra.codex_secondary_used_percent
})
const codexPrimaryResetAt = computed(() => {
const extra = props.account.extra
if (!extra || extra.codex_primary_reset_after_seconds === undefined) return null
const resetTime = new Date(Date.now() + extra.codex_primary_reset_after_seconds * 1000)
return resetTime.toISOString()
})
const codexSecondaryResetAt = computed(() => {
const extra = props.account.extra
if (!extra || extra.codex_secondary_reset_after_seconds === undefined) return null
const resetTime = new Date(Date.now() + extra.codex_secondary_reset_after_seconds * 1000)
return resetTime.toISOString()
})
const loadUsage = async () => {
// Only fetch usage for Anthropic OAuth accounts
// OpenAI doesn't have a usage window API - usage is updated from response headers during forwarding
// OpenAI usage comes from account.extra field (updated during forwarding)
if (props.account.platform !== 'anthropic' || props.account.type !== 'oauth') return
loading.value = true

View File

@@ -29,7 +29,7 @@
<transition name="dropdown">
<div
v-if="tooltipOpen"
class="absolute right-0 mt-2 w-80 bg-white dark:bg-dark-800 rounded-xl shadow-xl border border-gray-200 dark:border-dark-700 z-50"
class="absolute right-0 mt-2 w-[340px] bg-white dark:bg-dark-800 rounded-xl shadow-xl border border-gray-200 dark:border-dark-700 z-50 overflow-hidden"
>
<div class="p-3 border-b border-gray-100 dark:border-dark-700">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
@@ -62,43 +62,43 @@
<!-- Progress bars -->
<div class="space-y-1.5">
<div v-if="subscription.group?.daily_limit_usd" class="flex items-center gap-2">
<span class="text-[10px] text-gray-500 w-8">{{ t('subscriptionProgress.daily') }}</span>
<div class="flex-1 bg-gray-200 dark:bg-dark-600 rounded-full h-1.5">
<span class="text-[10px] text-gray-500 w-8 flex-shrink-0">{{ t('subscriptionProgress.daily') }}</span>
<div class="flex-1 min-w-0 bg-gray-200 dark:bg-dark-600 rounded-full h-1.5">
<div
class="h-1.5 rounded-full transition-all"
:class="getProgressBarClass(subscription.daily_usage_usd, subscription.group?.daily_limit_usd)"
:style="{ width: getProgressWidth(subscription.daily_usage_usd, subscription.group?.daily_limit_usd) }"
></div>
</div>
<span class="text-[10px] text-gray-500 w-16 text-right">
<span class="text-[10px] text-gray-500 w-24 text-right flex-shrink-0">
{{ formatUsage(subscription.daily_usage_usd, subscription.group?.daily_limit_usd) }}
</span>
</div>
<div v-if="subscription.group?.weekly_limit_usd" class="flex items-center gap-2">
<span class="text-[10px] text-gray-500 w-8">{{ t('subscriptionProgress.weekly') }}</span>
<div class="flex-1 bg-gray-200 dark:bg-dark-600 rounded-full h-1.5">
<span class="text-[10px] text-gray-500 w-8 flex-shrink-0">{{ t('subscriptionProgress.weekly') }}</span>
<div class="flex-1 min-w-0 bg-gray-200 dark:bg-dark-600 rounded-full h-1.5">
<div
class="h-1.5 rounded-full transition-all"
:class="getProgressBarClass(subscription.weekly_usage_usd, subscription.group?.weekly_limit_usd)"
:style="{ width: getProgressWidth(subscription.weekly_usage_usd, subscription.group?.weekly_limit_usd) }"
></div>
</div>
<span class="text-[10px] text-gray-500 w-16 text-right">
<span class="text-[10px] text-gray-500 w-24 text-right flex-shrink-0">
{{ formatUsage(subscription.weekly_usage_usd, subscription.group?.weekly_limit_usd) }}
</span>
</div>
<div v-if="subscription.group?.monthly_limit_usd" class="flex items-center gap-2">
<span class="text-[10px] text-gray-500 w-8">{{ t('subscriptionProgress.monthly') }}</span>
<div class="flex-1 bg-gray-200 dark:bg-dark-600 rounded-full h-1.5">
<span class="text-[10px] text-gray-500 w-8 flex-shrink-0">{{ t('subscriptionProgress.monthly') }}</span>
<div class="flex-1 min-w-0 bg-gray-200 dark:bg-dark-600 rounded-full h-1.5">
<div
class="h-1.5 rounded-full transition-all"
:class="getProgressBarClass(subscription.monthly_usage_usd, subscription.group?.monthly_limit_usd)"
:style="{ width: getProgressWidth(subscription.monthly_usage_usd, subscription.group?.monthly_limit_usd) }"
></div>
</div>
<span class="text-[10px] text-gray-500 w-16 text-right">
<span class="text-[10px] text-gray-500 w-24 text-right flex-shrink-0">
{{ formatUsage(subscription.monthly_usage_usd, subscription.group?.monthly_limit_usd) }}
</span>
</div>