feat(grok): 吸收调度阈值、配额解析、批量用量与 CLI 身份

从 personal-dev 取优并接线到 feat/grok-complete-integration:

- 调度阈值:grok_sched_* 写入、ApplyAccountSchedulingThreshold、
  account_scheduling_thresholds 设置与 Settings UI、网关过滤
- 配额头:x-rate-limit 别名、相对秒 reset、tier/entitlement 扩展
- 批量 usage:GetUsageBatch + POST /usage/batch + AccountsView 合并加载
- CLI 身份:xai.cli_identity 统一 pin/UA;service 层 applyGrokCLIHeaders
  与 transport 最终改写对齐;media eligibility 模块落地
- UsageCell:月度 cents→USD 与 30d 进度条展示

保留既有 failure taxonomy / sticky / free recovery 等 HEAD cools。
This commit is contained in:
IanShaw027
2026-08-07 14:53:18 +08:00
parent 0316994c52
commit 7c62382d04
55 changed files with 3542 additions and 162 deletions
@@ -393,6 +393,29 @@
: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 :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>
</div>
<UsageProgressBar
v-if="!grokWeeklyBillingBar && !grokIsFree && grokRequestQuotaBar"
:label="t('admin.accounts.usageWindow.grokRequests')"
@@ -652,16 +675,25 @@ const props = withDefaults(
todayStats?: WindowStats | null
todayStatsLoading?: boolean
manualRefreshToken?: number
batchedUsage?: AccountUsageInfo | null
batchedUsageError?: string | null
batchedUsageLoading?: boolean
requestBatchedUsage?: ((account: Account, options?: { force?: boolean }) => void) | null
}>(),
{
todayStats: null,
todayStatsLoading: false,
manualRefreshToken: 0
manualRefreshToken: 0,
batchedUsage: null,
batchedUsageError: null,
batchedUsageLoading: false,
requestBatchedUsage: null
}
)
const emit = defineEmits<{
'account-updated': [account: Account]
'usage-loaded': [usage: AccountUsageInfo]
}>()
const { t } = useI18n()
@@ -674,6 +706,9 @@ const loading = ref(false)
const activeQueryLoading = ref(false)
const error = ref<string | null>(null)
const usageInfo = ref<AccountUsageInfo | null>(null)
watch(usageInfo, (usage) => {
if (usage) emit('usage-loaded', usage)
})
const suppressOpenAIUsageRefreshUntil = ref(0)
const rootRef = ref<HTMLElement | null>(null)
const isDesktopViewport = ref(
@@ -713,6 +748,8 @@ const shouldFetchUsage = computed(() => {
return false
})
const isBatchManaged = computed(() => typeof props.requestBatchedUsage === 'function')
const showGeminiTodayStats = computed(() => {
return props.account.platform === 'gemini' && props.account.type === 'service_account'
})
@@ -1094,6 +1131,58 @@ const grokWeeklyBillingBar = computed((): GrokQuotaBarInfo | null => {
resetsAt: billing.period_end || null
}
})
// Monthly used/limit % from billing probe (used_percent or derived from cents).
const grokMonthlyBillingBar = computed((): GrokQuotaBarInfo | null => {
const billing = grokBilling.value
if (!billing) return null
let utilization: number | null = null
if (billing.used_percent != null && Number.isFinite(billing.used_percent)) {
utilization = billing.used_percent
} else if (
billing.monthly_limit_cents != null &&
billing.monthly_limit_cents > 0 &&
billing.used_cents != null
) {
utilization = (billing.used_cents / billing.monthly_limit_cents) * 100
}
if (utilization == null) return null
// Avoid duplicating the weekly bar when period_type is weekly-only without monthly.
if (billing.period_type?.toLowerCase() === 'weekly' && billing.monthly_limit_cents == null) {
return null
}
return {
utilization: Math.min(100, Math.max(0, utilization)),
resetsAt: billing.billing_period_end || billing.period_end || null
}
})
const formatGrokMoneyFromCents = (cents?: number | null) => {
if (cents == null || Number.isNaN(cents)) return '0'
const dollars = cents / 100
if (dollars >= 1000) return formatCompactNumber(dollars)
if (dollars >= 100) return dollars.toFixed(0)
if (dollars >= 10) return dollars.toFixed(1)
return dollars.toFixed(2)
}
// Absolute monthly used/limit derived from cents fields already on GrokBillingSummary.
// (personal-dev has separate prepaid/on_demand fields; HEAD only has cents.)
const grokBillingMoneySummary = computed(() => {
const billing = grokBilling.value
if (!billing) return null
const limit = billing.monthly_limit_cents
const used = billing.used_cents
if ((limit == null || limit <= 0) && (used == null || used <= 0)) 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)))
}
return {
used: formatGrokMoneyFromCents(used ?? 0),
limit: formatGrokMoneyFromCents(limit ?? 0),
usedPercent
}
})
const grokPlanLabelIsFree = (value: string) => value.includes('free') || value.includes('basic')
const grokPlanLabelIsPaid = (value: string) => {
return value !== '' && !grokPlanLabelIsFree(value) && !value.includes('unknown')
@@ -1136,7 +1225,16 @@ const grokFreeTokenBar = computed(() => {
})
const grokQuotaUnknown = computed(() => {
if (props.account.platform !== 'grok') return false
if (grokBilling.value || grokFreeTokenBar.value || grokRequestQuotaBar.value || grokTokenQuotaBar.value) return false
if (
grokBilling.value ||
grokBillingMoneySummary.value ||
grokMonthlyBillingBar.value ||
grokFreeTokenBar.value ||
grokRequestQuotaBar.value ||
grokTokenQuotaBar.value
) {
return false
}
return usageInfo.value?.grok_quota_snapshot_state !== 'observed'
})
const grokQuotaUnknownLabel = computed(() => {
@@ -1273,8 +1371,24 @@ const isAnthropicOAuthOrSetupToken = computed(() => {
return props.account.platform === 'anthropic' && (props.account.type === 'oauth' || props.account.type === 'setup-token')
})
const requestParentBatchUsage = (options?: { force?: boolean }) => {
if (!isBatchManaged.value || !shouldFetchUsage.value) return
props.requestBatchedUsage?.(props.account, options)
}
const syncManagedUsageState = () => {
if (!isBatchManaged.value) return
usageInfo.value = props.batchedUsage ?? null
error.value = props.batchedUsageError ?? null
loading.value = props.batchedUsageLoading === true
}
const loadUsage = async (options?: { source?: 'passive' | 'active'; bypassCache?: boolean }) => {
if (!shouldFetchUsage.value) return
if (isBatchManaged.value) {
requestParentBatchUsage({ force: options?.bypassCache === true })
return
}
// Check cache
if (!options?.bypassCache) {
@@ -1529,11 +1643,49 @@ onMounted(() => {
}
}
if (isBatchManaged.value) {
syncManagedUsageState()
requestParentBatchUsage()
return
}
if (!shouldAutoLoadUsageOnMount.value) return
const source = isAnthropicOAuthOrSetupToken.value ? 'passive' : undefined
requestAutoLoad(source)
})
watch(
() => [props.batchedUsage, props.batchedUsageError, props.batchedUsageLoading, isBatchManaged.value] as const,
() => {
syncManagedUsageState()
},
{ immediate: true, deep: true }
)
watch(isBatchManaged, (managed, wasManaged) => {
if (managed && !wasManaged) {
syncManagedUsageState()
requestParentBatchUsage()
}
})
watch(
() => [props.account.id, props.account.platform, props.account.type, isBatchManaged.value] as const,
([accountID, platform, accountType, managed], [previousAccountID, previousPlatform, previousAccountType]) => {
if (
accountID === previousAccountID &&
platform === previousPlatform &&
accountType === previousAccountType
) {
return
}
if (!managed || !shouldFetchUsage.value) return
syncManagedUsageState()
requestParentBatchUsage()
},
{ flush: 'post' }
)
watch(openAIUsageRefreshKey, (nextKey, prevKey) => {
if (!prevKey || nextKey === prevKey) return
if (props.account.platform !== 'openai' || props.account.type !== 'oauth') return
@@ -1542,6 +1694,11 @@ watch(openAIUsageRefreshKey, (nextKey, prevKey) => {
return
}
if (isBatchManaged.value) {
requestParentBatchUsage({ force: true })
return
}
_usageCache.delete(props.account.id)
requestAutoLoad()
})
@@ -1552,6 +1709,11 @@ watch(
if (nextToken === prevToken) return
if (!shouldFetchUsage.value) return
if (isBatchManaged.value) {
requestParentBatchUsage({ force: true })
return
}
const source = isAnthropicOAuthOrSetupToken.value ? 'passive' : undefined
_usageCache.delete(props.account.id)
loadUsage({ source, bypassCache: true }).catch((e) => {