mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 09:50:21 +08:00
feat(grok): add admin oauth and quota support
This commit is contained in:
30
frontend/src/utils/__tests__/providerKeyAuth.spec.ts
Normal file
30
frontend/src/utils/__tests__/providerKeyAuth.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
@@ -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)')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]'
|
||||
|
||||
@@ -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':
|
||||
|
||||
Reference in New Issue
Block a user