mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(pool): 新增账号配额展示并清理 Codex 旧限额字段
- 后端池子 Key 列表新增 account_quota 字段,按 provider_type 生成配额摘要(codex/kiro/antigravity) - 前端 PoolManagement 新增“配额”列与进度条展示,桌面端与移动端同步支持 - Provider 详情页移除 Codex code_review 限额展示,仅保留周限额与 5H 限额 - 清理 codex usage parser/realtime quota 中 code_review 相关解析与比较逻辑 - 同步更新调度与配额相关测试,改为忽略无关历史字段
This commit is contained in:
@@ -78,6 +78,7 @@ export interface PoolKeyDetail {
|
||||
key_name: string
|
||||
is_active: boolean
|
||||
auth_type: string
|
||||
account_quota: string | null
|
||||
cooldown_reason: string | null
|
||||
cooldown_ttl_seconds: number | null
|
||||
cost_window_usage: number
|
||||
|
||||
@@ -284,10 +284,6 @@ export interface CodexUpstreamMetadata {
|
||||
secondary_reset_seconds?: number // 5H限额重置剩余秒数
|
||||
secondary_reset_at?: number // 5H限额重置时间(Unix 时间戳)
|
||||
secondary_window_minutes?: number // 5H限额窗口大小(分钟)
|
||||
code_review_used_percent?: number // 代码审查限额使用百分比
|
||||
code_review_reset_seconds?: number // 代码审查限额重置剩余秒数
|
||||
code_review_reset_at?: number // 代码审查限额重置时间(Unix 时间戳)
|
||||
code_review_window_minutes?: number // 代码审查限额窗口大小(分钟)
|
||||
has_credits?: boolean // 是否有积分
|
||||
credits_balance?: number // 积分余额
|
||||
}
|
||||
|
||||
@@ -534,10 +534,10 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 限额并排显示:Team/Plus/Enterprise 账号 3列, Free 账号 2列 -->
|
||||
<!-- 限额并排显示:Team/Plus/Enterprise 账号 2列, Free 账号 1列 -->
|
||||
<div
|
||||
class="grid gap-3"
|
||||
:class="isCodexTeamPlan(key) ? 'grid-cols-3' : 'grid-cols-2'"
|
||||
:class="isCodexTeamPlan(key) ? 'grid-cols-2' : 'grid-cols-1'"
|
||||
>
|
||||
<!-- 周限额 -->
|
||||
<div v-if="key.upstream_metadata.codex?.primary_used_percent !== undefined">
|
||||
@@ -585,28 +585,6 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 代码审查限额 -->
|
||||
<div v-if="key.upstream_metadata.codex?.code_review_used_percent !== undefined">
|
||||
<div class="flex items-center justify-between text-[10px] mb-0.5">
|
||||
<span class="text-muted-foreground">审查限额</span>
|
||||
<span :class="getQuotaRemainingClass(key.upstream_metadata.codex.code_review_used_percent)">
|
||||
{{ (100 - key.upstream_metadata.codex.code_review_used_percent).toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
class="absolute left-0 top-0 h-full transition-all duration-300"
|
||||
:class="getQuotaRemainingBarColor(key.upstream_metadata.codex.code_review_used_percent)"
|
||||
:style="{ width: `${Math.max(100 - key.upstream_metadata.codex.code_review_used_percent, 0)}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="key.upstream_metadata.codex.code_review_reset_seconds"
|
||||
class="text-[9px] text-muted-foreground/70 mt-0.5"
|
||||
>
|
||||
{{ formatResetTime(key.upstream_metadata.codex.code_review_reset_seconds) }}后重置
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Antigravity 上游额度摘要(按家族分组展示关键配额) -->
|
||||
|
||||
@@ -290,6 +290,12 @@
|
||||
<TableHead class="font-semibold">
|
||||
名称
|
||||
</TableHead>
|
||||
<TableHead
|
||||
v-if="showAccountQuotaColumn"
|
||||
class="w-64 font-semibold"
|
||||
>
|
||||
配额
|
||||
</TableHead>
|
||||
<TableHead class="w-20 font-semibold">
|
||||
状态
|
||||
</TableHead>
|
||||
@@ -330,6 +336,51 @@
|
||||
{{ key.key_name || '未命名' }}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
v-if="showAccountQuotaColumn"
|
||||
class="py-3"
|
||||
>
|
||||
<div
|
||||
v-if="quotaProgressMap[key.key_id]?.length"
|
||||
class="flex items-stretch gap-2 overflow-x-auto pb-0.5"
|
||||
>
|
||||
<div
|
||||
v-for="(item, idx) in quotaProgressMap[key.key_id]"
|
||||
:key="`${key.key_id}-quota-${idx}`"
|
||||
class="min-w-[140px] max-w-[180px] shrink-0"
|
||||
>
|
||||
<div class="flex items-center justify-between text-[10px] mb-0.5">
|
||||
<span class="text-muted-foreground">{{ item.label }}</span>
|
||||
<span :class="getQuotaRemainingClassByRemaining(item.remainingPercent)">
|
||||
{{ item.remainingPercent.toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
class="absolute left-0 top-0 h-full transition-all duration-300"
|
||||
:class="getQuotaRemainingBarColorByRemaining(item.remainingPercent)"
|
||||
:style="{ width: `${item.remainingPercent}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="item.detail"
|
||||
class="text-[9px] text-muted-foreground/70 mt-0.5 truncate"
|
||||
>
|
||||
{{ item.detail }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-else-if="key.account_quota"
|
||||
:class="getQuotaTextClass(key.account_quota)"
|
||||
>
|
||||
{{ key.account_quota }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-xs text-muted-foreground"
|
||||
>-</span>
|
||||
</TableCell>
|
||||
<TableCell class="py-3">
|
||||
<Badge
|
||||
:variant="key.is_active ? (key.cooldown_reason ? 'destructive' : 'default') : 'secondary'"
|
||||
@@ -488,7 +539,10 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2.5 ml-7 grid grid-cols-3 gap-2">
|
||||
<div
|
||||
class="mt-2.5 ml-7 grid gap-2"
|
||||
:class="showAccountQuotaColumn ? 'grid-cols-4' : 'grid-cols-3'"
|
||||
>
|
||||
<div class="p-2 bg-muted/50 rounded-lg text-xs">
|
||||
<div class="text-muted-foreground mb-0.5">
|
||||
冷却
|
||||
@@ -543,6 +597,56 @@
|
||||
{{ key.last_used_at ? formatRelativeTime(key.last_used_at) : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="showAccountQuotaColumn"
|
||||
class="p-2 bg-muted/50 rounded-lg text-xs"
|
||||
>
|
||||
<div class="text-muted-foreground mb-0.5">
|
||||
配额
|
||||
</div>
|
||||
<div
|
||||
v-if="quotaProgressMap[key.key_id]?.length"
|
||||
class="flex items-stretch gap-2 overflow-x-auto pb-0.5"
|
||||
>
|
||||
<div
|
||||
v-for="(item, idx) in quotaProgressMap[key.key_id]"
|
||||
:key="`${key.key_id}-quota-mobile-${idx}`"
|
||||
class="min-w-[120px] max-w-[160px] shrink-0"
|
||||
>
|
||||
<div class="flex items-center justify-between text-[10px] mb-0.5">
|
||||
<span class="text-muted-foreground">{{ item.label }}</span>
|
||||
<span :class="getQuotaRemainingClassByRemaining(item.remainingPercent)">
|
||||
{{ item.remainingPercent.toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
class="absolute left-0 top-0 h-full transition-all duration-300"
|
||||
:class="getQuotaRemainingBarColorByRemaining(item.remainingPercent)"
|
||||
:style="{ width: `${item.remainingPercent}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="item.detail"
|
||||
class="text-[9px] text-muted-foreground/70 mt-0.5 truncate"
|
||||
>
|
||||
{{ item.detail }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="key.account_quota"
|
||||
:class="getQuotaTextClass(key.account_quota)"
|
||||
>
|
||||
{{ key.account_quota }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
-
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -682,6 +786,19 @@ const selectedProviderConfig = computed<PoolAdvancedConfig | null>(() => {
|
||||
return (selectedProviderData.value as Record<string, unknown> | null)?.pool_advanced as PoolAdvancedConfig | null ?? null
|
||||
})
|
||||
|
||||
const selectedProviderType = computed(() => {
|
||||
const fromDetail = String(selectedProviderData.value?.provider_type || '').trim().toLowerCase()
|
||||
if (fromDetail) return fromDetail
|
||||
const fromOverview = poolProviders.value.find(item => item.provider_id === selectedProviderId.value)?.provider_type
|
||||
return String(fromOverview || '').trim().toLowerCase()
|
||||
})
|
||||
|
||||
const showAccountQuotaColumn = computed(() => {
|
||||
return selectedProviderType.value === 'codex'
|
||||
|| selectedProviderType.value === 'kiro'
|
||||
|| selectedProviderType.value === 'antigravity'
|
||||
})
|
||||
|
||||
async function selectProvider(id: string) {
|
||||
selectedProviderId.value = id
|
||||
selectedKeys.value.clear()
|
||||
@@ -714,6 +831,20 @@ const statusFilter = ref('all')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(50)
|
||||
|
||||
interface QuotaProgressItem {
|
||||
label: string
|
||||
remainingPercent: number
|
||||
detail?: string
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
async function loadKeys() {
|
||||
if (!selectedProviderId.value) return
|
||||
keysLoading.value = true
|
||||
@@ -840,6 +971,70 @@ function getCostBarColor(usage: number, limit: number): string {
|
||||
return 'bg-green-500'
|
||||
}
|
||||
|
||||
function normalizeQuotaLabel(label: string): string {
|
||||
const normalized = label.trim()
|
||||
if (!normalized) return '额度'
|
||||
if (normalized.includes('周剩余')) return '周限额'
|
||||
if (normalized.includes('5H剩余')) return '5H限额'
|
||||
if (normalized.includes('最低剩余')) return '最低剩余'
|
||||
if (normalized === '剩余') return '剩余额度'
|
||||
return normalized
|
||||
}
|
||||
|
||||
function clampPercent(value: number): number {
|
||||
if (!Number.isFinite(value)) return 0
|
||||
if (value < 0) return 0
|
||||
if (value > 100) return 100
|
||||
return value
|
||||
}
|
||||
|
||||
function parseQuotaProgressItems(quotaText: string | null | undefined): QuotaProgressItem[] {
|
||||
if (!quotaText) return []
|
||||
|
||||
const segments = quotaText
|
||||
.split('|')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const items: QuotaProgressItem[] = []
|
||||
for (const segment of segments) {
|
||||
const match = segment.match(/^(.*?)(-?\d+(?:\.\d+)?)%\s*(.*)$/)
|
||||
if (!match) continue
|
||||
|
||||
const [, rawLabel, rawPercent, rawTail] = match
|
||||
const remainingPercent = clampPercent(Number(rawPercent))
|
||||
const label = normalizeQuotaLabel(rawLabel)
|
||||
const detail = rawTail.trim().replace(/^[()]+|[()]+$/g, '').trim()
|
||||
|
||||
items.push({
|
||||
label,
|
||||
remainingPercent,
|
||||
detail: detail || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
function getQuotaRemainingClassByRemaining(remaining: number): string {
|
||||
if (remaining <= 10) return 'text-red-600 dark:text-red-400'
|
||||
if (remaining <= 30) return 'text-yellow-600 dark:text-yellow-400'
|
||||
return 'text-green-600 dark:text-green-400'
|
||||
}
|
||||
|
||||
function getQuotaRemainingBarColorByRemaining(remaining: number): string {
|
||||
if (remaining <= 10) return 'bg-red-500 dark:bg-red-400'
|
||||
if (remaining <= 30) return 'bg-yellow-500 dark:bg-yellow-400'
|
||||
return 'bg-green-500 dark:bg-green-400'
|
||||
}
|
||||
|
||||
function getQuotaTextClass(quotaText: string): string {
|
||||
if (quotaText.includes('封禁') || quotaText.includes('受限')) {
|
||||
return 'text-[11px] text-destructive leading-4'
|
||||
}
|
||||
return 'text-[11px] text-foreground/90 leading-4'
|
||||
}
|
||||
|
||||
function formatRelativeTime(isoStr: string): string {
|
||||
const diff = (Date.now() - new Date(isoStr).getTime()) / 1000
|
||||
if (diff < 60) return '刚刚'
|
||||
|
||||
Reference in New Issue
Block a user