mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-29 16:20:27 +00:00
fix(admin): simplify Grok usage UI for free 24h and paid windows
Show free accounts only the rolling 24h soft-gate bar; paid accounts show 7d/30d and prepaid money. Drop the always-visible manual probe chip from the usage cell in favor of scheduled recovery and usage load.
This commit is contained in:
@@ -113,8 +113,6 @@
|
||||
<!-- No data yet -->
|
||||
<div v-else class="space-y-1">
|
||||
<div class="text-xs text-gray-400">-</div>
|
||||
<!-- Always allow on-demand upstream quota probe, even before passive headers exist. -->
|
||||
<GrokQuotaProbeCell :account="account" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -356,120 +354,72 @@
|
||||
</div>
|
||||
<div v-else-if="isForbidden" class="space-y-1">
|
||||
<span class="inline-block rounded px-1.5 py-0.5 text-[10px] font-medium bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300">
|
||||
{{ grokEntitlementLabel || t('admin.accounts.forbidden') }}
|
||||
{{ usageInfo?.grok_entitlement_status || t('admin.accounts.forbidden') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else-if="usageInfo" class="space-y-1">
|
||||
<div v-if="grokEntitlementLabel" class="mb-0.5">
|
||||
<span class="inline-block rounded bg-zinc-100 px-1.5 py-0.5 text-[10px] font-medium text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200">
|
||||
{{ grokEntitlementLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="showGrokStandaloneLocalStats && grokLocalUsage" class="mb-0.5 flex items-center">
|
||||
<div class="flex items-center gap-1.5 text-[9px] text-gray-500 dark:text-gray-400">
|
||||
<span class="rounded bg-gray-100 px-1.5 py-0.5 dark:bg-gray-800">
|
||||
{{ formatWindowRequests(grokLocalUsage) }} req
|
||||
</span>
|
||||
<span class="rounded bg-gray-100 px-1.5 py-0.5 dark:bg-gray-800">
|
||||
{{ formatWindowTokens(grokLocalUsage) }}
|
||||
</span>
|
||||
<span class="rounded bg-gray-100 px-1.5 py-0.5 dark:bg-gray-800" :title="t('usage.accountBilled')">
|
||||
A ${{ formatWindowCost(grokLocalUsage) }}
|
||||
</span>
|
||||
<!-- Free: only rolling 24h soft-gate bar. Paid: 7d + 30d + prepaid money. -->
|
||||
<template v-if="grokIsFree">
|
||||
<UsageProgressBar
|
||||
v-if="grokFreeTokenBar"
|
||||
label="24h"
|
||||
:title="t('admin.accounts.usageWindow.grokFreeQuota24hHint', { limit: formatCompactNumber(grokFreeTokenBar.limit) })"
|
||||
:utilization="grokFreeTokenBar.utilization"
|
||||
:show-now-when-idle="true"
|
||||
color="emerald"
|
||||
/>
|
||||
<div v-else-if="grokQuotaUnknown" class="text-[10px] text-gray-500 dark:text-gray-400">
|
||||
{{ grokQuotaUnknownLabel }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<UsageProgressBar
|
||||
v-if="grokWeeklyBillingBar"
|
||||
label="7d"
|
||||
:utilization="grokWeeklyBillingBar.utilization"
|
||||
:resets-at="grokWeeklyBillingBar.resetsAt"
|
||||
:show-now-when-idle="true"
|
||||
color="indigo"
|
||||
/>
|
||||
<UsageProgressBar
|
||||
v-if="grokMonthlyBillingBar"
|
||||
label="30d"
|
||||
:utilization="grokMonthlyBillingBar.utilization"
|
||||
:resets-at="grokMonthlyBillingBar.resetsAt"
|
||||
:show-now-when-idle="true"
|
||||
color="indigo"
|
||||
/>
|
||||
<div
|
||||
v-if="grokPrepaidMoneyLine"
|
||||
class="flex flex-wrap items-center gap-1 text-[10px] text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<span
|
||||
v-if="grokLocalUsage.user_cost != null"
|
||||
class="rounded bg-gray-100 px-1.5 py-0.5 dark:bg-gray-800"
|
||||
:title="t('usage.userBilled')"
|
||||
class="rounded bg-emerald-50 px-1 py-0.5 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300"
|
||||
:title="t('admin.accounts.usageWindow.grokPrepaid')"
|
||||
>
|
||||
U ${{ formatWindowUserCost(grokLocalUsage) }}
|
||||
{{ t('admin.accounts.usageWindow.grokPrepaid') }} ${{ grokPrepaidMoneyLine.prepaid }}
|
||||
</span>
|
||||
<span :title="t('admin.accounts.usageWindow.grokMonthlyLimit')">
|
||||
{{ t('admin.accounts.usageWindow.grokUsed') }}
|
||||
{{ grokPrepaidMoneyLine.used }}/{{ grokPrepaidMoneyLine.limit }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="grokQuotaUnknown" class="text-[10px] text-gray-500 dark:text-gray-400">
|
||||
{{ grokQuotaUnknownLabel }}
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="usageInfo.error" class="truncate text-xs text-amber-600 dark:text-amber-400 max-w-[200px]" :title="usageInfo.error">
|
||||
{{ usageErrorLabel }}
|
||||
</div>
|
||||
<UsageProgressBar
|
||||
v-if="grokWeeklyBillingBar"
|
||||
label="7d"
|
||||
:utilization="grokWeeklyBillingBar.utilization"
|
||||
:resets-at="grokWeeklyBillingBar.resetsAt"
|
||||
:show-now-when-idle="true"
|
||||
color="indigo"
|
||||
/>
|
||||
<UsageProgressBar
|
||||
v-if="grokMonthlyBillingBar"
|
||||
label="30d"
|
||||
:utilization="grokMonthlyBillingBar.utilization"
|
||||
:resets-at="grokMonthlyBillingBar.resetsAt"
|
||||
:show-now-when-idle="true"
|
||||
color="indigo"
|
||||
/>
|
||||
<div
|
||||
v-if="grokBillingMoneySummary"
|
||||
class="flex flex-wrap items-center gap-1 text-[10px] text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<span
|
||||
v-if="grokBillingMoneySummary.prepaid != null"
|
||||
class="rounded bg-emerald-50 px-1 py-0.5 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300"
|
||||
:title="t('admin.accounts.usageWindow.grokPrepaid')"
|
||||
>
|
||||
{{ t('admin.accounts.usageWindow.grokPrepaid') }} ${{ grokBillingMoneySummary.prepaid }}
|
||||
</span>
|
||||
<span :title="t('admin.accounts.usageWindow.grokMonthlyLimit')">
|
||||
{{ t('admin.accounts.usageWindow.grokUsed') }}
|
||||
{{ grokBillingMoneySummary.used }}/{{ grokBillingMoneySummary.limit }}
|
||||
</span>
|
||||
<span
|
||||
v-if="grokBillingMoneySummary.usedPercent != null"
|
||||
class="rounded bg-gray-100 px-1 py-0.5 dark:bg-gray-800"
|
||||
>
|
||||
{{ grokBillingMoneySummary.usedPercent }}%
|
||||
</span>
|
||||
<span
|
||||
v-if="grokBillingMoneySummary.showOverage"
|
||||
class="rounded bg-amber-50 px-1 py-0.5 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300"
|
||||
:title="t('admin.accounts.usageWindow.grokOverage')"
|
||||
>
|
||||
{{ t('admin.accounts.usageWindow.grokOverageShort') }}
|
||||
{{ grokBillingMoneySummary.onDemandUsed }}/{{ grokBillingMoneySummary.onDemandCap }}
|
||||
</span>
|
||||
</div>
|
||||
<UsageProgressBar
|
||||
v-if="!grokWeeklyBillingBar && !grokIsFree && grokRequestQuotaBar"
|
||||
:label="t('admin.accounts.usageWindow.grokRequests')"
|
||||
:utilization="grokRequestQuotaBar.utilization"
|
||||
:resets-at="grokRequestQuotaBar.resetsAt"
|
||||
:remaining-capacity="true"
|
||||
color="indigo"
|
||||
/>
|
||||
<UsageProgressBar
|
||||
v-if="!grokWeeklyBillingBar && !grokIsFree && grokTokenQuotaBar"
|
||||
:label="t('admin.accounts.usageWindow.grokTokens')"
|
||||
:utilization="grokTokenQuotaBar.utilization"
|
||||
:resets-at="grokTokenQuotaBar.resetsAt"
|
||||
:remaining-capacity="true"
|
||||
color="emerald"
|
||||
/>
|
||||
<UsageProgressBar
|
||||
v-if="grokFreeTokenBar"
|
||||
label="24h"
|
||||
:title="t('admin.accounts.usageWindow.grokFreeQuota24hHint', { limit: formatCompactNumber(grokFreeTokenBar.limit) })"
|
||||
:utilization="grokFreeTokenBar.utilization"
|
||||
:show-now-when-idle="true"
|
||||
color="emerald"
|
||||
/>
|
||||
<div v-if="grokRetryAfterLabel" class="text-[10px] text-amber-600 dark:text-amber-400">
|
||||
{{ t('admin.accounts.usageWindow.grokRetryAfter', { time: grokRetryAfterLabel }) }}
|
||||
</div>
|
||||
<div v-if="grokQuotaUnknown" class="text-[10px] text-gray-500 dark:text-gray-400">
|
||||
{{ grokQuotaUnknownLabel }}
|
||||
</div>
|
||||
<div v-else-if="usageInfo.error" class="truncate text-xs text-amber-600 dark:text-amber-400 max-w-[200px]" :title="usageInfo.error">
|
||||
{{ usageErrorLabel }}
|
||||
</div>
|
||||
<div v-if="grokQuotaStatusLine" class="text-[10px] text-gray-500 dark:text-gray-400">
|
||||
{{ grokQuotaStatusLine }}
|
||||
</div>
|
||||
<GrokQuotaProbeCell :account="account" @probed="handleGrokProbed" />
|
||||
<GrokQuotaProbeCell :account="account" compact @probed="handleGrokProbed" />
|
||||
</div>
|
||||
<div v-else class="space-y-1">
|
||||
<div class="text-xs text-gray-400">-</div>
|
||||
<GrokQuotaProbeCell :account="account" compact @probed="handleGrokProbed" />
|
||||
</div>
|
||||
<div v-else class="text-xs text-gray-400">-</div>
|
||||
</template>
|
||||
|
||||
<!-- Gemini platform: show quota + local usage window -->
|
||||
@@ -667,11 +617,10 @@
|
||||
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { GrokQuotaProbeResult } from '@/api/admin/grok'
|
||||
import type { Account, AccountUsageInfo, GeminiCredentials, WindowStats } from '@/types'
|
||||
import { buildOpenAIUsageRefreshKey } from '@/utils/accountUsageRefresh'
|
||||
import { enqueueUsageRequest } from '@/utils/usageLoadQueue'
|
||||
import { formatCompactNumber, formatRelativeTime } from '@/utils/format'
|
||||
import { formatCompactNumber } from '@/utils/format'
|
||||
import UsageProgressBar from './UsageProgressBar.vue'
|
||||
import AccountQuotaInfo from './AccountQuotaInfo.vue'
|
||||
import OpenAIQuotaResetCell from './OpenAIQuotaResetCell.vue'
|
||||
@@ -1124,17 +1073,6 @@ interface GrokQuotaBarInfo {
|
||||
resetsAt: string | null
|
||||
}
|
||||
|
||||
const makeGrokQuotaBar = (quota?: { limit?: number | null; remaining?: number | null; reset_at?: string | null } | null): GrokQuotaBarInfo | null => {
|
||||
if (!quota || quota.limit == null || quota.remaining == null || quota.limit <= 0) return null
|
||||
const remaining = Math.min(quota.limit, Math.max(0, quota.remaining))
|
||||
return {
|
||||
utilization: (remaining / quota.limit) * 100,
|
||||
resetsAt: quota.reset_at || null
|
||||
}
|
||||
}
|
||||
|
||||
const grokRequestQuotaBar = computed(() => makeGrokQuotaBar(usageInfo.value?.grok_request_quota))
|
||||
const grokTokenQuotaBar = computed(() => makeGrokQuotaBar(usageInfo.value?.grok_token_quota))
|
||||
const grokBilling = computed(() => usageInfo.value?.grok_billing || null)
|
||||
const grokWeeklyBillingBar = computed((): GrokQuotaBarInfo | null => {
|
||||
const billing = grokBilling.value
|
||||
@@ -1177,49 +1115,30 @@ const formatGrokMoney = (value?: number | null) => {
|
||||
if (value >= 10) return value.toFixed(1)
|
||||
return value.toFixed(2)
|
||||
}
|
||||
// Absolute money: prefer prepaid/monthly_used fields; fall back to cents/100.
|
||||
const grokBillingMoneySummary = computed(() => {
|
||||
// Prepaid money line for paid Grok: show when prepaid_balance is present.
|
||||
// Monthly used/limit numbers are optional context; primary progress is the 30d bar.
|
||||
const grokPrepaidMoneyLine = computed(() => {
|
||||
const billing = grokBilling.value
|
||||
if (!billing) return null
|
||||
const prepaid =
|
||||
billing.prepaid_balance != null
|
||||
? billing.prepaid_balance
|
||||
: null
|
||||
const prepaid = billing.prepaid_balance
|
||||
// "只针对预付": only render when prepaid field exists (including $0.00).
|
||||
if (prepaid == null || !Number.isFinite(prepaid)) return null
|
||||
const used =
|
||||
billing.monthly_used != null
|
||||
? billing.monthly_used
|
||||
: billing.used_cents != null
|
||||
? billing.used_cents / 100
|
||||
: null
|
||||
: 0
|
||||
const limit =
|
||||
billing.monthly_limit != null
|
||||
? billing.monthly_limit
|
||||
: billing.monthly_limit_cents != null
|
||||
? billing.monthly_limit_cents / 100
|
||||
: null
|
||||
const onDemandUsed = billing.on_demand_used ?? null
|
||||
const onDemandCap = billing.on_demand_cap ?? null
|
||||
const hasAny =
|
||||
(prepaid ?? 0) > 0 ||
|
||||
(used ?? 0) > 0 ||
|
||||
(limit ?? 0) > 0 ||
|
||||
(onDemandCap ?? 0) > 0 ||
|
||||
(onDemandUsed ?? 0) > 0
|
||||
if (!hasAny) return null
|
||||
let usedPercent: number | null = null
|
||||
if (billing.used_percent != null && Number.isFinite(billing.used_percent)) {
|
||||
usedPercent = Math.round(Math.min(100, Math.max(0, billing.used_percent)))
|
||||
} else if (limit != null && limit > 0 && used != null) {
|
||||
usedPercent = Math.round(Math.min(100, Math.max(0, (used / limit) * 100)))
|
||||
}
|
||||
: 0
|
||||
return {
|
||||
prepaid: prepaid != null ? formatGrokMoney(prepaid) : null,
|
||||
used: formatGrokMoney(used ?? 0),
|
||||
limit: formatGrokMoney(limit ?? 0),
|
||||
onDemandUsed: formatGrokMoney(onDemandUsed ?? 0),
|
||||
onDemandCap: formatGrokMoney(onDemandCap ?? 0),
|
||||
showOverage: (onDemandCap ?? 0) > 0 || (onDemandUsed ?? 0) > 0,
|
||||
usedPercent
|
||||
prepaid: formatGrokMoney(prepaid),
|
||||
used: formatGrokMoney(used),
|
||||
limit: formatGrokMoney(limit)
|
||||
}
|
||||
})
|
||||
const grokPlanLabelIsFree = (value: string) => value.includes('free') || value.includes('basic')
|
||||
@@ -1247,18 +1166,6 @@ const grokIsFree = computed(() => {
|
||||
return billing != null
|
||||
})
|
||||
const grokFreeQuotaUsage = computed(() => usageInfo.value?.grok_local_usage_24h || null)
|
||||
const showGrokStandaloneLocalStats = computed(() => {
|
||||
// Official 7d/30d bars already embed window_stats — avoid duplicate local row.
|
||||
return !usageInfo.value?.seven_day && !usageInfo.value?.thirty_day
|
||||
})
|
||||
const grokLocalUsage = computed(() => {
|
||||
if (grokIsFree.value) return grokFreeQuotaUsage.value
|
||||
return props.todayStats ||
|
||||
usageInfo.value?.grok_local_usage ||
|
||||
usageInfo.value?.grok_local_usage_7d ||
|
||||
usageInfo.value?.grok_local_usage_monthly ||
|
||||
null
|
||||
})
|
||||
const grokFreeTokenBar = computed(() => {
|
||||
if (!grokIsFree.value || !grokFreeQuotaUsage.value) return null
|
||||
const limit = usageInfo.value?.grok_free_token_limit
|
||||
@@ -1268,14 +1175,10 @@ const grokFreeTokenBar = computed(() => {
|
||||
})
|
||||
const grokQuotaUnknown = computed(() => {
|
||||
if (props.account.platform !== 'grok') return false
|
||||
if (
|
||||
grokBilling.value ||
|
||||
grokBillingMoneySummary.value ||
|
||||
grokMonthlyBillingBar.value ||
|
||||
grokFreeTokenBar.value ||
|
||||
grokRequestQuotaBar.value ||
|
||||
grokTokenQuotaBar.value
|
||||
) {
|
||||
if (grokIsFree.value) {
|
||||
return !grokFreeTokenBar.value
|
||||
}
|
||||
if (grokWeeklyBillingBar.value || grokMonthlyBillingBar.value || grokPrepaidMoneyLine.value) {
|
||||
return false
|
||||
}
|
||||
return usageInfo.value?.grok_quota_snapshot_state !== 'observed'
|
||||
@@ -1285,33 +1188,6 @@ const grokQuotaUnknownLabel = computed(() => {
|
||||
? t('admin.accounts.usageWindow.grokNoHeaders')
|
||||
: t('admin.accounts.usageWindow.grokUnknown')
|
||||
})
|
||||
const grokQuotaStatusLine = computed(() => {
|
||||
if (props.account.platform !== 'grok') return null
|
||||
const parts: string[] = []
|
||||
const status = usageInfo.value?.grok_last_status_code
|
||||
if (status) {
|
||||
parts.push(t('admin.accounts.usageWindow.grokLastStatus', { status }))
|
||||
}
|
||||
if (usageInfo.value?.grok_last_quota_probe_at) {
|
||||
parts.push(
|
||||
t('admin.accounts.usageWindow.grokLastProbe', {
|
||||
time: formatRelativeTime(usageInfo.value.grok_last_quota_probe_at)
|
||||
})
|
||||
)
|
||||
}
|
||||
if (usageInfo.value?.grok_last_headers_seen_at) {
|
||||
parts.push(
|
||||
t('admin.accounts.usageWindow.grokLastHeadersSeen', {
|
||||
time: formatRelativeTime(usageInfo.value.grok_last_headers_seen_at)
|
||||
})
|
||||
)
|
||||
}
|
||||
return parts.length > 0 ? parts.join(' | ') : null
|
||||
})
|
||||
const grokEntitlementLabel = computed(() => {
|
||||
const status = (usageInfo.value?.grok_entitlement_status || '').trim()
|
||||
return status || null
|
||||
})
|
||||
const grokRetryAfterLabel = computed(() => {
|
||||
const seconds = usageInfo.value?.grok_retry_after_seconds
|
||||
if (seconds == null || seconds <= 0) return null
|
||||
@@ -1320,11 +1196,6 @@ const grokRetryAfterLabel = computed(() => {
|
||||
return `${minutes}m`
|
||||
})
|
||||
|
||||
const formatWindowRequests = (stats: WindowStats) => formatCompactNumber(stats.requests, { allowBillions: false })
|
||||
const formatWindowTokens = (stats: WindowStats) => formatCompactNumber(stats.tokens)
|
||||
const formatWindowCost = (stats: WindowStats) => stats.cost.toFixed(2)
|
||||
const formatWindowUserCost = (stats: WindowStats) => (stats.user_cost ?? 0).toFixed(2)
|
||||
|
||||
// 账户类型显示标签
|
||||
const antigravityTierLabel = computed(() => {
|
||||
switch (antigravityTier.value) {
|
||||
@@ -1447,9 +1318,9 @@ const loadUsage = async (options?: { source?: 'passive' | 'active'; bypassCache?
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const fetchFn = () => options?.source
|
||||
? adminAPI.accounts.getUsage(props.account.id, options.source)
|
||||
: adminAPI.accounts.getUsage(props.account.id)
|
||||
const fetchFn = () => options?.source
|
||||
? adminAPI.accounts.getUsage(props.account.id, options.source, options.bypassCache === true)
|
||||
: adminAPI.accounts.getUsage(props.account.id)
|
||||
const result = await enqueueUsageRequest(props.account, fetchFn)
|
||||
if (!unmounted.value) {
|
||||
usageInfo.value = result
|
||||
@@ -1526,54 +1397,10 @@ const loadActiveUsage = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleGrokProbed = (result: GrokQuotaProbeResult) => {
|
||||
const current = usageInfo.value
|
||||
if (!current) return
|
||||
const snapshot = result.snapshot
|
||||
const statusCode = snapshot?.status_code ?? result.status_code
|
||||
const hasActiveProbeSnapshot = snapshot != null && (
|
||||
result.source === 'active_probe' ||
|
||||
result.source === 'hybrid_probe' ||
|
||||
snapshot.observation_source === 'active_probe'
|
||||
)
|
||||
const probeSucceeded = hasActiveProbeSnapshot &&
|
||||
statusCode != null && statusCode >= 200 && statusCode < 300
|
||||
const snapshotEntitlement = snapshot?.entitlement_status?.trim()
|
||||
const currentEntitlement = current.grok_entitlement_status?.trim()
|
||||
const entitlementStatus = snapshotEntitlement || (
|
||||
probeSucceeded && currentEntitlement?.toLowerCase() === 'forbidden'
|
||||
? undefined
|
||||
: current.grok_entitlement_status
|
||||
)
|
||||
const merged: AccountUsageInfo = {
|
||||
...current,
|
||||
grok_billing: result.billing ?? current.grok_billing,
|
||||
grok_local_usage_24h: result.local_usage_24h ?? current.grok_local_usage_24h,
|
||||
grok_local_usage_7d: result.local_usage_7d ?? current.grok_local_usage_7d,
|
||||
grok_local_usage_monthly: result.local_usage_monthly ?? current.grok_local_usage_monthly,
|
||||
grok_request_quota: snapshot?.requests ?? current.grok_request_quota,
|
||||
grok_token_quota: snapshot?.tokens ?? current.grok_token_quota,
|
||||
grok_retry_after_seconds: snapshot?.retry_after_seconds ?? current.grok_retry_after_seconds,
|
||||
grok_entitlement_status: entitlementStatus,
|
||||
grok_quota_snapshot_state: result.billing
|
||||
? 'billing_observed'
|
||||
: snapshot?.headers_observed
|
||||
? 'observed'
|
||||
: current.grok_quota_snapshot_state,
|
||||
grok_last_quota_probe_at: result.billing?.fetched_at ?? snapshot?.last_probe_at ?? current.grok_last_quota_probe_at,
|
||||
grok_last_headers_seen_at: snapshot?.last_headers_seen_at ?? current.grok_last_headers_seen_at,
|
||||
grok_last_status_code: result.status_code ?? snapshot?.status_code ?? current.grok_last_status_code,
|
||||
is_forbidden: probeSucceeded ? false : current.is_forbidden,
|
||||
forbidden_reason: probeSucceeded ? undefined : current.forbidden_reason,
|
||||
forbidden_type: probeSucceeded ? undefined : current.forbidden_type,
|
||||
validation_url: probeSucceeded ? undefined : current.validation_url,
|
||||
needs_verify: probeSucceeded ? false : current.needs_verify,
|
||||
is_banned: probeSucceeded ? false : current.is_banned,
|
||||
error: result.billing || snapshot ? undefined : current.error,
|
||||
error_code: result.billing || snapshot ? undefined : current.error_code
|
||||
}
|
||||
usageInfo.value = merged
|
||||
_usageCache.set(props.account.id, { data: merged, ts: Date.now() })
|
||||
// The probe persists upstream quota state; refresh this cell so its compact
|
||||
// bars and entitlement status reflect the newly observed snapshot.
|
||||
const handleGrokProbed = async () => {
|
||||
await loadUsage({ source: 'active', bypassCache: true })
|
||||
}
|
||||
|
||||
// ===== API Key quota progress bars =====
|
||||
|
||||
@@ -24,18 +24,13 @@
|
||||
</svg>
|
||||
{{ t('admin.accounts.usageWindow.grokProbe') }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex cursor-not-allowed items-center gap-0.5 rounded px-1.5 py-0.5 text-[10px] font-medium text-gray-400 opacity-70 dark:text-gray-500"
|
||||
disabled
|
||||
:title="t('admin.accounts.usageWindow.grokResetUnsupportedTooltip')"
|
||||
>
|
||||
{{ t('admin.accounts.usageWindow.grokResetUnsupported') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="summary" class="text-[10px] text-gray-600 dark:text-gray-300">
|
||||
<!-- Compact mode: parent already shows 7d/30d/prepaid or 24h — only surface errors. -->
|
||||
<div
|
||||
v-if="!compact && summary"
|
||||
class="text-[10px] text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
{{ summary }}
|
||||
</div>
|
||||
<div v-if="error" class="truncate text-[10px] text-red-600 dark:text-red-400" :title="error">
|
||||
@@ -48,12 +43,17 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { GrokQuotaProbeResult, GrokQuotaWindow } from '@/api/admin/grok'
|
||||
import type { GrokQuotaProbeResult } from '@/api/admin/grok'
|
||||
import type { Account } from '@/types'
|
||||
|
||||
const props = defineProps<{
|
||||
account: Account
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
account: Account
|
||||
/** When true, only show the probe button (+ errors). No duplicate weekly summary. */
|
||||
compact?: boolean
|
||||
}>(),
|
||||
{ compact: false }
|
||||
)
|
||||
|
||||
const emit = defineEmits<{ probed: [result: GrokQuotaProbeResult] }>()
|
||||
|
||||
@@ -79,42 +79,16 @@ const extractErrorMessage = (e: unknown): string => {
|
||||
)
|
||||
}
|
||||
|
||||
const formatWindow = (label: string, window?: GrokQuotaWindow | null): string | null => {
|
||||
if (!window || window.limit == null || window.remaining == null) return null
|
||||
return `${label} ${window.remaining}/${window.limit}`
|
||||
}
|
||||
|
||||
const retryAfterLabel = computed(() => {
|
||||
const seconds = data.value?.snapshot?.retry_after_seconds
|
||||
if (seconds == null || seconds <= 0) return null
|
||||
if (seconds < 60) return `${seconds}s`
|
||||
return `${Math.ceil(seconds / 60)}m`
|
||||
})
|
||||
|
||||
const summary = computed(() => {
|
||||
const snapshot = data.value?.snapshot
|
||||
if (!data.value) return ''
|
||||
if (props.compact || !data.value) return ''
|
||||
// Non-compact fallback (rarely used): brief weekly percent if present.
|
||||
const billing = data.value.billing
|
||||
const parts: Array<string | null> = []
|
||||
if (billing?.period_type?.toLowerCase() === 'weekly' && billing.usage_percent != null) {
|
||||
parts.push(t('admin.accounts.usageWindow.grokWeeklyUsage', {
|
||||
return t('admin.accounts.usageWindow.grokWeeklyUsage', {
|
||||
percent: Math.round(Math.min(100, Math.max(0, billing.usage_percent)))
|
||||
}))
|
||||
})
|
||||
}
|
||||
if (snapshot) {
|
||||
parts.push(
|
||||
formatWindow(t('admin.accounts.usageWindow.grokRequests'), snapshot.requests),
|
||||
formatWindow(t('admin.accounts.usageWindow.grokTokens'), snapshot.tokens)
|
||||
)
|
||||
}
|
||||
if (retryAfterLabel.value) {
|
||||
parts.push(t('admin.accounts.usageWindow.grokRetryAfter', { time: retryAfterLabel.value }))
|
||||
}
|
||||
if (snapshot?.entitlement_status) {
|
||||
parts.push(snapshot.entitlement_status)
|
||||
}
|
||||
const visibleParts = parts.filter((part): part is string => Boolean(part))
|
||||
return visibleParts.length > 0 ? visibleParts.join(' | ') : t('admin.accounts.usageWindow.grokNoHeaders')
|
||||
return ''
|
||||
})
|
||||
|
||||
const truncatedError = computed(() => {
|
||||
|
||||
@@ -651,7 +651,7 @@ describe('AccountUsageCell', () => {
|
||||
expect(badges.some(node => node.attributes('title') === 'usage.userBilled')).toBe(true)
|
||||
})
|
||||
|
||||
it('Grok OAuth 会展示本地 user billed 用量并把耗尽配额显示为 0% 剩余', async () => {
|
||||
it('Grok OAuth compact UI drops local chips and header quota bars', async () => {
|
||||
getUsage.mockResolvedValue({
|
||||
grok_local_usage: {
|
||||
requests: 4,
|
||||
@@ -670,12 +670,7 @@ describe('AccountUsageCell', () => {
|
||||
|
||||
const wrapper = mount(AccountUsageCell, {
|
||||
props: {
|
||||
account: makeAccount({
|
||||
id: 3861,
|
||||
platform: 'grok',
|
||||
type: 'oauth',
|
||||
extra: {}
|
||||
})
|
||||
account: makeAccount({ id: 3861, platform: 'grok', type: 'oauth', extra: {} })
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
@@ -683,66 +678,49 @@ describe('AccountUsageCell', () => {
|
||||
props: ['label', 'utilization', 'resetsAt', 'color'],
|
||||
template: '<div class="usage-bar">{{ label }}|{{ utilization }}|{{ resetsAt }}</div>'
|
||||
},
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: true
|
||||
AccountQuotaInfo: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(getUsage).toHaveBeenCalledWith(3861)
|
||||
expect(wrapper.text()).toContain('4 req')
|
||||
expect(wrapper.text()).toContain('1.2K')
|
||||
expect(wrapper.text()).toContain('A $0.12')
|
||||
expect(wrapper.text()).toContain('U $0.34')
|
||||
expect(wrapper.text()).toContain('admin.accounts.usageWindow.grokRequests|0|2026-07-09T16:00:00Z')
|
||||
|
||||
const badges = wrapper.findAll('span[title]')
|
||||
expect(badges.some(node => node.attributes('title') === 'usage.accountBilled')).toBe(true)
|
||||
expect(badges.some(node => node.attributes('title') === 'usage.userBilled')).toBe(true)
|
||||
expect(wrapper.text()).not.toContain('4 req')
|
||||
expect(wrapper.text()).not.toContain('admin.accounts.usageWindow.grokRequests|')
|
||||
})
|
||||
|
||||
it('Grok OAuth 配额条按剩余容量显示 100% 满格和 25% 低量', async () => {
|
||||
it('Grok paid monthly limits show 30d bar without free 24h', async () => {
|
||||
getUsage.mockResolvedValue({
|
||||
grok_request_quota: {
|
||||
limit: 100,
|
||||
remaining: 100,
|
||||
reset_at: '2026-07-09T16:00:00Z'
|
||||
grok_billing: {
|
||||
period_type: 'weekly',
|
||||
usage_percent: null,
|
||||
used_percent: 12,
|
||||
monthly_limit_cents: 25_000,
|
||||
used_cents: 3_000,
|
||||
plan: ''
|
||||
},
|
||||
grok_token_quota: {
|
||||
limit: 1000,
|
||||
remaining: 250,
|
||||
reset_at: '2026-07-09T16:00:00Z'
|
||||
},
|
||||
grok_quota_snapshot_state: 'observed'
|
||||
grok_entitlement_status: 'free',
|
||||
grok_token_quota: { limit: 1_000, remaining: 250 }
|
||||
})
|
||||
|
||||
const wrapper = mount(AccountUsageCell, {
|
||||
props: {
|
||||
account: makeAccount({
|
||||
id: 4073,
|
||||
platform: 'grok',
|
||||
type: 'oauth',
|
||||
extra: {}
|
||||
})
|
||||
account: makeAccount({ id: 4402, platform: 'grok', type: 'oauth', extra: {} })
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
UsageProgressBar: {
|
||||
props: ['label', 'utilization', 'resetsAt', 'color', 'remainingCapacity'],
|
||||
template: '<div class="usage-bar">{{ label }}|{{ utilization }}|{{ remainingCapacity }}</div>'
|
||||
props: ['label', 'utilization'],
|
||||
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
|
||||
},
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: true
|
||||
AccountQuotaInfo: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('admin.accounts.usageWindow.grokRequests|100|true')
|
||||
expect(wrapper.text()).toContain('admin.accounts.usageWindow.grokTokens|25|true')
|
||||
expect(wrapper.text()).toContain('30d|')
|
||||
expect(wrapper.text()).not.toContain('24h|')
|
||||
})
|
||||
|
||||
it('Grok OAuth uses the official weekly billing percentage when available', async () => {
|
||||
@@ -775,7 +753,6 @@ describe('AccountUsageCell', () => {
|
||||
template: '<div class="usage-bar">{{ label }}|{{ utilization }}|{{ resetsAt }}|{{ remainingCapacity }}</div>'
|
||||
},
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: true
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -789,11 +766,11 @@ describe('AccountUsageCell', () => {
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ tokens: 0, expected: 0, compact: '0' },
|
||||
{ tokens: 500_000, expected: 50, compact: '500.0K' },
|
||||
{ tokens: 1_000_000, expected: 100, compact: '1.0M' },
|
||||
{ tokens: 1_100_000, expected: 100, compact: '1.1M' }
|
||||
])('Grok Free derives its 1M quota from local tokens: $tokens -> $expected%', async ({ tokens, expected, compact }) => {
|
||||
{ tokens: 0, expected: 0 },
|
||||
{ tokens: 500_000, expected: 50 },
|
||||
{ tokens: 1_000_000, expected: 100 },
|
||||
{ tokens: 1_100_000, expected: 100 }
|
||||
])('Grok Free derives its 1M quota from local tokens: $tokens -> $expected%', async ({ tokens, expected }) => {
|
||||
getUsage.mockResolvedValue({
|
||||
grok_free_token_limit: 1_000_000,
|
||||
grok_billing: {
|
||||
@@ -823,7 +800,6 @@ describe('AccountUsageCell', () => {
|
||||
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
|
||||
},
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: true
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -831,10 +807,10 @@ describe('AccountUsageCell', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain(`24h|${expected}`)
|
||||
expect(wrapper.findAll('span').filter((node) => node.text() === compact)).toHaveLength(1)
|
||||
expect(wrapper.findAll('.usage-bar')).toHaveLength(1)
|
||||
expect(wrapper.text()).not.toContain('admin.accounts.usageWindow.grokRequests|')
|
||||
expect(wrapper.text()).not.toContain('admin.accounts.usageWindow.grokTokens|')
|
||||
expect(wrapper.text()).not.toContain('7d|')
|
||||
})
|
||||
|
||||
it('Grok Free uses rolling 24h usage instead of today-only usage', async () => {
|
||||
@@ -872,7 +848,6 @@ describe('AccountUsageCell', () => {
|
||||
template: '<div class="usage-bar">{{ label }}|{{ utilization }}|{{ title }}</div>'
|
||||
},
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: true
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -880,7 +855,6 @@ describe('AccountUsageCell', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('24h|75|admin.accounts.usageWindow.grokFreeQuota24hHint')
|
||||
expect(wrapper.text()).toContain('750.0K')
|
||||
expect(wrapper.text()).not.toContain('7d|')
|
||||
expect(wrapper.text()).not.toContain('200.0K')
|
||||
expect(wrapper.text()).not.toContain('250.0K')
|
||||
@@ -917,7 +891,6 @@ describe('AccountUsageCell', () => {
|
||||
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
|
||||
},
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: true
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -930,85 +903,6 @@ describe('AccountUsageCell', () => {
|
||||
expect(wrapper.text()).not.toContain('250.0K')
|
||||
})
|
||||
|
||||
it('Grok paid plans are not mistaken for Free when weekly usage is temporarily missing', async () => {
|
||||
getUsage.mockResolvedValue({
|
||||
grok_billing: {
|
||||
period_type: 'weekly',
|
||||
usage_percent: null,
|
||||
plan: 'SuperGrok Heavy'
|
||||
},
|
||||
grok_entitlement_status: 'free',
|
||||
grok_local_usage: {
|
||||
requests: 2,
|
||||
tokens: 2_000_000,
|
||||
cost: 1,
|
||||
standard_cost: 1
|
||||
},
|
||||
grok_token_quota: { limit: 1_000, remaining: 250 }
|
||||
})
|
||||
|
||||
const wrapper = mount(AccountUsageCell, {
|
||||
props: {
|
||||
account: makeAccount({ id: 4401, platform: 'grok', type: 'oauth', extra: {} })
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
UsageProgressBar: {
|
||||
props: ['label', 'utilization'],
|
||||
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
|
||||
},
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('admin.accounts.usageWindow.grokTokens|25')
|
||||
expect(wrapper.text()).not.toContain('2M|')
|
||||
})
|
||||
|
||||
it('Grok custom paid monthly limits override stale Free entitlement', async () => {
|
||||
getUsage.mockResolvedValue({
|
||||
grok_billing: {
|
||||
period_type: 'weekly',
|
||||
usage_percent: null,
|
||||
monthly_limit_cents: 25_000,
|
||||
plan: ''
|
||||
},
|
||||
grok_entitlement_status: 'free',
|
||||
grok_local_usage: {
|
||||
requests: 2,
|
||||
tokens: 2_000_000,
|
||||
cost: 1,
|
||||
standard_cost: 1
|
||||
},
|
||||
grok_token_quota: { limit: 1_000, remaining: 250 }
|
||||
})
|
||||
|
||||
const wrapper = mount(AccountUsageCell, {
|
||||
props: {
|
||||
account: makeAccount({ id: 4402, platform: 'grok', type: 'oauth', extra: {} })
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
UsageProgressBar: {
|
||||
props: ['label', 'utilization'],
|
||||
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
|
||||
},
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('admin.accounts.usageWindow.grokTokens|25')
|
||||
expect(wrapper.text()).not.toContain('2M|')
|
||||
})
|
||||
|
||||
it('Grok credential Free tier keeps the 1M fallback when billing is unavailable', async () => {
|
||||
getUsage.mockResolvedValue({
|
||||
grok_free_token_limit: 1_000_000,
|
||||
@@ -1032,7 +926,6 @@ describe('AccountUsageCell', () => {
|
||||
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
|
||||
},
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: true
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1042,257 +935,6 @@ describe('AccountUsageCell', () => {
|
||||
expect(wrapper.text()).toContain('24h|100')
|
||||
})
|
||||
|
||||
it('Grok paid manual probes keep the weekly/local summary when 24h usage is returned', async () => {
|
||||
getUsage.mockResolvedValue({
|
||||
grok_quota_snapshot_state: 'no_headers',
|
||||
error: 'stale error',
|
||||
error_code: 'quota_unknown'
|
||||
})
|
||||
|
||||
const wrapper = mount(AccountUsageCell, {
|
||||
props: {
|
||||
account: makeAccount({ id: 4501, platform: 'grok', type: 'oauth', extra: {} })
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
UsageProgressBar: {
|
||||
props: ['label', 'utilization', 'resetsAt'],
|
||||
template: '<div class="usage-bar">{{ label }}|{{ utilization }}|{{ resetsAt }}</div>'
|
||||
},
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: {
|
||||
emits: ['probed'],
|
||||
template: `<button class="probe" @click="$emit('probed', {
|
||||
source: 'hybrid_probe',
|
||||
billing: { period_type: 'weekly', usage_percent: 42, period_end: '2026-07-17T00:00:00Z' },
|
||||
snapshot: {
|
||||
headers_observed: true,
|
||||
updated_at: '2026-07-13T00:00:00Z',
|
||||
entitlement_status: 'ACTIVE',
|
||||
requests: { limit: 100, remaining: 20 }
|
||||
},
|
||||
local_usage_24h: { requests: 3, tokens: 750000, cost: 0.75, standard_cost: 0.75, user_cost: 0.25 },
|
||||
local_usage_7d: { requests: 4, tokens: 1000000, cost: 1, standard_cost: 1, user_cost: 0.5 },
|
||||
local_usage_monthly: { requests: 7, tokens: 1500000, cost: 2, standard_cost: 2, user_cost: 1 },
|
||||
status_code: 200,
|
||||
headers_observed: true,
|
||||
reset_supported: false,
|
||||
fetched_at: 1
|
||||
})">probe</button>`
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
await wrapper.get('.probe').trigger('click')
|
||||
|
||||
expect(wrapper.text()).toContain('7d|42|2026-07-17T00:00:00Z')
|
||||
expect(wrapper.text()).toContain('1.0M')
|
||||
expect(wrapper.text()).not.toContain('750.0K')
|
||||
expect(wrapper.text()).toContain('ACTIVE')
|
||||
expect(wrapper.text()).not.toContain('stale error')
|
||||
})
|
||||
|
||||
it('Grok successful probes immediately clear stale forbidden state', async () => {
|
||||
getUsage.mockResolvedValue({
|
||||
is_forbidden: true,
|
||||
forbidden_reason: 'stale forbidden response',
|
||||
forbidden_type: 'validation',
|
||||
validation_url: 'https://example.com/verify',
|
||||
needs_verify: true,
|
||||
is_banned: true,
|
||||
grok_entitlement_status: 'forbidden',
|
||||
grok_quota_snapshot_state: 'no_headers',
|
||||
error: 'stale forbidden response',
|
||||
error_code: 'forbidden'
|
||||
})
|
||||
|
||||
const wrapper = mount(AccountUsageCell, {
|
||||
props: {
|
||||
account: makeAccount({ id: 4503, platform: 'grok', type: 'oauth', extra: {} })
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
UsageProgressBar: true,
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('forbidden')
|
||||
|
||||
const setupState = wrapper.vm.$.setupState as {
|
||||
handleGrokProbed: (result: Record<string, unknown>) => void
|
||||
usageInfo: Record<string, unknown> | null
|
||||
}
|
||||
setupState.handleGrokProbed({
|
||||
source: 'active_probe',
|
||||
snapshot: {
|
||||
headers_observed: false,
|
||||
updated_at: '2026-07-18T00:00:00Z',
|
||||
status_code: 200
|
||||
},
|
||||
status_code: 200,
|
||||
headers_observed: false,
|
||||
reset_supported: false,
|
||||
fetched_at: 1
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(setupState.usageInfo).toMatchObject({
|
||||
is_forbidden: false,
|
||||
needs_verify: false,
|
||||
is_banned: false,
|
||||
grok_last_status_code: 200
|
||||
})
|
||||
expect(setupState.usageInfo?.forbidden_reason).toBeUndefined()
|
||||
expect(setupState.usageInfo?.forbidden_type).toBeUndefined()
|
||||
expect(setupState.usageInfo?.validation_url).toBeUndefined()
|
||||
expect(setupState.usageInfo?.grok_entitlement_status).toBeUndefined()
|
||||
expect(wrapper.text()).not.toContain('admin.accounts.forbidden')
|
||||
})
|
||||
|
||||
it('Grok successful probes preserve the entitlement reported by the latest snapshot', async () => {
|
||||
getUsage.mockResolvedValue({
|
||||
is_forbidden: true,
|
||||
grok_entitlement_status: 'forbidden'
|
||||
})
|
||||
|
||||
const wrapper = mount(AccountUsageCell, {
|
||||
props: {
|
||||
account: makeAccount({ id: 4504, platform: 'grok', type: 'oauth', extra: {} })
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
UsageProgressBar: true,
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
const setupState = wrapper.vm.$.setupState as {
|
||||
handleGrokProbed: (result: Record<string, unknown>) => void
|
||||
usageInfo: Record<string, unknown> | null
|
||||
}
|
||||
setupState.handleGrokProbed({
|
||||
source: 'active_probe',
|
||||
snapshot: {
|
||||
headers_observed: true,
|
||||
updated_at: '2026-07-18T00:00:00Z',
|
||||
entitlement_status: 'ACTIVE',
|
||||
status_code: 200
|
||||
},
|
||||
status_code: 200,
|
||||
headers_observed: true,
|
||||
reset_supported: false,
|
||||
fetched_at: 1
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(setupState.usageInfo?.grok_entitlement_status).toBe('ACTIVE')
|
||||
expect(wrapper.text()).toContain('ACTIVE')
|
||||
expect(wrapper.text()).not.toContain('admin.accounts.forbidden')
|
||||
})
|
||||
|
||||
it('Grok billing-only success does not clear an active-probe forbidden state', async () => {
|
||||
getUsage.mockResolvedValue({
|
||||
is_forbidden: true,
|
||||
forbidden_type: 'forbidden',
|
||||
needs_verify: true,
|
||||
is_banned: true,
|
||||
grok_entitlement_status: 'forbidden'
|
||||
})
|
||||
|
||||
const wrapper = mount(AccountUsageCell, {
|
||||
props: {
|
||||
account: makeAccount({ id: 4505, platform: 'grok', type: 'oauth', extra: {} })
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
UsageProgressBar: true,
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
const setupState = wrapper.vm.$.setupState as {
|
||||
handleGrokProbed: (result: Record<string, unknown>) => void
|
||||
usageInfo: Record<string, unknown> | null
|
||||
}
|
||||
setupState.handleGrokProbed({
|
||||
source: 'billing_probe',
|
||||
billing: {
|
||||
period_type: 'weekly',
|
||||
usage_percent: 10,
|
||||
plan: 'SuperGrok'
|
||||
},
|
||||
status_code: 200,
|
||||
headers_observed: false,
|
||||
reset_supported: false,
|
||||
fetched_at: 1
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(setupState.usageInfo).toMatchObject({
|
||||
is_forbidden: true,
|
||||
forbidden_type: 'forbidden',
|
||||
needs_verify: true,
|
||||
is_banned: true,
|
||||
grok_entitlement_status: 'forbidden'
|
||||
})
|
||||
expect(wrapper.text()).toContain('forbidden')
|
||||
})
|
||||
|
||||
it('Grok Free manual probes merge rolling 24h usage', async () => {
|
||||
getUsage.mockResolvedValue({
|
||||
grok_free_token_limit: 1_000_000,
|
||||
subscription_tier: 'FREE',
|
||||
grok_quota_snapshot_state: 'no_headers'
|
||||
})
|
||||
|
||||
const wrapper = mount(AccountUsageCell, {
|
||||
props: {
|
||||
account: makeAccount({ id: 4502, platform: 'grok', type: 'oauth', extra: {} })
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
UsageProgressBar: {
|
||||
props: ['label', 'utilization'],
|
||||
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
|
||||
},
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: {
|
||||
emits: ['probed'],
|
||||
template: `<button class="probe" @click="$emit('probed', {
|
||||
source: 'hybrid_probe',
|
||||
billing: { period_type: 'weekly', usage_percent: null, plan: '' },
|
||||
local_usage_24h: { requests: 12, tokens: 750000, cost: 0, standard_cost: 0 },
|
||||
headers_observed: false,
|
||||
reset_supported: false,
|
||||
fetched_at: 1
|
||||
})">probe</button>`
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
await wrapper.get('.probe').trigger('click')
|
||||
|
||||
expect(wrapper.text()).toContain('24h|75')
|
||||
expect(wrapper.text()).toContain('750.0K')
|
||||
expect(wrapper.text()).not.toContain('7d|')
|
||||
})
|
||||
|
||||
it('Key 账号在 today stats loading 时显示骨架屏', async () => {
|
||||
const wrapper = mount(AccountUsageCell, {
|
||||
props: {
|
||||
@@ -1424,7 +1066,6 @@ describe('AccountUsageCell', () => {
|
||||
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
|
||||
},
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: true
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1468,7 +1109,6 @@ describe('AccountUsageCell', () => {
|
||||
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
|
||||
},
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user