feat(grok): add admin oauth and quota support

This commit is contained in:
mayrain
2026-05-16 19:23:09 +08:00
parent cbfe1d378f
commit 936e1ae37b
61 changed files with 4748 additions and 287 deletions

View File

@@ -0,0 +1,30 @@
import { describe, expect, it } from 'vitest'
import {
getProviderMaskedSecretLabel,
shouldShowOAuthRefreshControl,
} from '@/utils/providerKeyAuth'
describe('providerKeyAuth', () => {
it('renders Grok OAuth-managed cookies as sessions without OAuth refresh controls', () => {
const key = {
auth_type: 'oauth',
oauth_managed: true,
can_refresh_oauth: false,
}
expect(getProviderMaskedSecretLabel(key, 'grok')).toBe('[Session Cookie]')
expect(shouldShowOAuthRefreshControl(key, 'grok')).toBe(false)
})
it('keeps standard OAuth providers on OAuth token semantics', () => {
const key = {
auth_type: 'oauth',
oauth_managed: true,
can_refresh_oauth: false,
}
expect(getProviderMaskedSecretLabel(key, 'codex')).toBe('[OAuth Token]')
expect(shouldShowOAuthRefreshControl(key, 'codex')).toBe(true)
})
})

View File

@@ -39,4 +39,69 @@ describe('providerKeyQuota', () => {
},
}, 'codex')).toBe('周剩余 90.0% | 5H剩余 80.0% | Spark5H剩余 60.0% | Spark周剩余 95.0%')
})
it('formats Grok account quota from structured quota windows', () => {
expect(getQuotaDisplayText({
status_snapshot: {
oauth: {
code: 'valid',
},
account: {
code: 'ok',
blocked: false,
},
quota: {
provider_type: 'grok',
code: 'ok',
exhausted: false,
windows: [
{
scope: 'account',
used_value: 2,
limit_value: 10,
remaining_ratio: 0.8,
},
],
},
},
}, 'grok')).toBe('剩余 80.0% (8/10)')
})
it('formats Grok mode quota from model-scoped windows', () => {
expect(getQuotaDisplayText({
status_snapshot: {
oauth: {
code: 'valid',
},
account: {
code: 'ok',
blocked: false,
},
quota: {
provider_type: 'grok',
code: 'ok',
exhausted: false,
plan_type: 'heavy',
windows: [
{
code: 'model:quota_auto',
label: 'auto',
scope: 'model',
remaining_ratio: 0.4,
used_value: 90,
limit_value: 150,
},
{
code: 'model:quota_heavy',
label: 'heavy',
scope: 'model',
remaining_ratio: 0,
used_value: 20,
limit_value: 20,
},
],
},
},
}, 'grok')).toBe('Auto剩余 40.0% (60/150) | Heavy剩余 0.0% (0/20)')
})
})

View File

@@ -4,6 +4,7 @@ export const OAUTH_ICONS: Record<string, string> = {
github: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>`,
google: `<svg viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>`,
gemini_cli: `<svg viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>`,
grok: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="10" fill="#111827"/><path d="M7 7L17 17M17 7L7 17" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round"/></svg>`,
}
// Default icon when provider type is not found

View File

@@ -8,6 +8,9 @@ const PLAN_TYPE_LABELS: Record<string, string> = {
'pro+': 'Pro+',
power: 'Power',
ultra: 'Ultra',
basic: 'Basic',
super: 'Super',
heavy: 'Heavy',
}
const PLAN_TYPE_CLASS_NAMES: Record<string, string> = {
@@ -20,6 +23,9 @@ const PLAN_TYPE_CLASS_NAMES: Record<string, string> = {
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',
}
export function normalizeOAuthPlanType(planType?: string | null): string | null {

View File

@@ -1,4 +1,5 @@
export interface ProviderKeyAuthCarrier {
provider_type?: string | null
auth_type?: string | null
credential_kind?: string | null
runtime_auth_kind?: string | null
@@ -15,6 +16,14 @@ function normalizeText(value: unknown): string | null {
return text || null
}
function resolveProviderType(input: ProviderKeyAuthCarrier, providerType?: string | null): string | null {
return normalizeText(providerType) ?? normalizeText(input.provider_type)
}
function isGrokSessionCredential(input: ProviderKeyAuthCarrier, providerType?: string | null): boolean {
return resolveProviderType(input, providerType) === 'grok' && isOAuthManagedCredential(input)
}
export function getProviderCredentialKind(
input: ProviderKeyAuthCarrier,
): 'raw_secret' | 'oauth_session' | 'service_account' {
@@ -78,7 +87,11 @@ export function canRefreshOAuthCredential(input: ProviderKeyAuthCarrier): boolea
return isOAuthManagedCredential(input)
}
export function shouldShowOAuthRefreshControl(input: ProviderKeyAuthCarrier): boolean {
export function shouldShowOAuthRefreshControl(
input: ProviderKeyAuthCarrier,
providerType?: string | null,
): boolean {
if (isGrokSessionCredential(input, providerType)) return false
return isOAuthManagedCredential(input)
}
@@ -103,7 +116,11 @@ export function getProviderAuthLabel(input: ProviderKeyAuthCarrier): string {
return getProviderRuntimeAuthKind(input) === 'bearer' ? 'Bearer' : 'API Key'
}
export function getProviderMaskedSecretLabel(input: ProviderKeyAuthCarrier): string {
export function getProviderMaskedSecretLabel(
input: ProviderKeyAuthCarrier,
providerType?: string | null,
): string {
if (isGrokSessionCredential(input, providerType)) return '[Session Cookie]'
if (isOAuthManagedCredential(input)) return '[OAuth Token]'
if (isServiceAccountCredential(input)) return '[Service Account]'
if (getProviderRuntimeAuthKind(input) === 'mixed') return '[Key]'

View File

@@ -94,6 +94,37 @@ function formatQuotaValue(value: number | null | undefined): string {
return normalized.toFixed(1)
}
function getQuotaWindowValueText(window: QuotaWindowSnapshot | null | undefined): string | null {
if (!window || typeof window.limit_value !== 'number' || window.limit_value <= 0) return null
if (typeof window.remaining_value === 'number') {
return `${formatQuotaValue(window.remaining_value)}/${formatQuotaValue(window.limit_value)}`
}
if (typeof window.used_value === 'number') {
return `${formatQuotaValue(Math.max(window.limit_value - window.used_value, 0))}/${formatQuotaValue(window.limit_value)}`
}
return null
}
const GROK_QUOTA_MODE_LABELS: Record<string, string> = {
quota_auto: 'Auto',
auto: 'Auto',
quota_fast: 'Fast',
fast: 'Fast',
quota_expert: 'Expert',
expert: 'Expert',
quota_heavy: 'Heavy',
heavy: 'Heavy',
quota_grok_4_3: 'Grok 4.3',
'grok-420-computer-use-sa': 'Grok 4.3',
}
function getGrokQuotaWindowLabel(window: QuotaWindowSnapshot): string {
const rawCode = normalizeText(window.code)?.replace(/^model:/i, '') || ''
const rawLabel = normalizeText(window.label) || normalizeText(window.model) || rawCode
const normalized = (rawLabel || rawCode).trim().toLowerCase()
return GROK_QUOTA_MODE_LABELS[normalized] || GROK_QUOTA_MODE_LABELS[rawCode.toLowerCase()] || rawLabel || rawCode || '模式'
}
function getCodexQuotaText(quota: QuotaStatusSnapshot): string | null {
const parts: string[] = []
for (const [label, code] of [
@@ -142,6 +173,47 @@ function getKiroQuotaText(quota: QuotaStatusSnapshot): string | null {
return normalizeText(quota.label)
}
function getGrokQuotaText(quota: QuotaStatusSnapshot): string | null {
const code = normalizeText(quota.code)?.toLowerCase()
if (code === 'banned') {
return normalizeText(quota.label) || '账号已封禁'
}
if (code === 'forbidden') {
return normalizeText(quota.label) || '访问受限'
}
const modelWindows = getQuotaWindowsByScope(quota, 'model')
const modelParts = modelWindows
.map((window) => {
const remainingPercent = getQuotaWindowRemainingPercent(window)
if (remainingPercent == null) return null
const valueText = getQuotaWindowValueText(window)
return `${getGrokQuotaWindowLabel(window)}剩余 ${formatPercent(remainingPercent)}${valueText ? ` (${valueText})` : ''}`
})
.filter((value): value is string => value != null)
if (modelParts.length > 0) return modelParts.join(' | ')
const window = getQuotaWindow(quota, 'usage') ?? getQuotaWindowsByScope(quota, 'account')[0] ?? null
const remainingPercent = getQuotaWindowRemainingPercent(window)
if (typeof window?.remaining_value === 'number' && typeof window.limit_value === 'number' && window.limit_value > 0 && window.remaining_value <= 0) {
return `剩余 ${formatQuotaValue(window.remaining_value)}/${formatQuotaValue(window.limit_value)}`
}
if (remainingPercent != null) {
const valueText = getQuotaWindowValueText(window)
if (valueText) {
return `剩余 ${formatPercent(remainingPercent)} (${valueText})`
}
return `剩余 ${formatPercent(remainingPercent)}`
}
if (typeof window?.remaining_value === 'number' && typeof window.limit_value === 'number' && window.limit_value > 0) {
return `剩余 ${formatQuotaValue(window.remaining_value)}/${formatQuotaValue(window.limit_value)}`
}
return normalizeText(quota.label)
}
function getAntigravityQuotaText(quota: QuotaStatusSnapshot): string | null {
const code = normalizeText(quota.code)?.toLowerCase()
if (code === 'forbidden') {
@@ -238,6 +310,8 @@ export function getQuotaSnapshotFallbackText(
return getCodexQuotaText(quota)
case 'kiro':
return getKiroQuotaText(quota)
case 'grok':
return getGrokQuotaText(quota)
case 'antigravity':
return getAntigravityQuotaText(quota)
case 'gemini_cli':