mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 08:27:46 +08:00
feat(providers): add xAI provider with device code OAuth
Add a separate `xai` provider type for xAI Grok CLI subscription accounts. It is independent of the existing `grok` provider, which reverse-proxies grok.com with browser cookies; behavior of `grok` is unchanged. Account binding uses the xAI device code flow, so no local callback listener is needed and headless deployments can bind accounts. Refresh tokens can also be imported individually or in batches, and are rotated on refresh. OAuth requests default to the cli-chat-proxy Responses API; API keys and compact stay on api.x.ai. Explicit custom gateways are preserved. Only `openai:responses` and `openai:responses:compact` are exposed; Chat, Claude and Gemini clients reach the provider through Aether's existing cross-format conversion rather than new native endpoints. Upstream Responses payloads are sanitized for what xAI actually rejects: `previous_response_id` and `metadata.user_id` are dropped, hosted `tool_choice` is rewritten, `web_search` is restored for converted clients, `image_generation` is stripped on older Grok conversation models, unsupported reasoning effort is removed, and requested `reasoning.encrypted_content` is preserved with a replay policy keyed on the configured provider type rather than the model name. Quota refresh reads /user and /billing?format=credits and stores a structured usage snapshot; a prepaid balance keeps an account selectable after the weekly allowance is exhausted. API-key accounts skip the subscription billing surface. The admin UI shows remaining weekly quota as a labeled bar in the provider drawer and the pool list. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -376,7 +376,7 @@ function jsonValueContainsAgentIdentity(value: unknown): boolean {
|
||||
export interface DeviceAuthorizeRequest {
|
||||
start_url?: string
|
||||
region?: string
|
||||
auth_type?: 'builder_id' | 'identity_center' | 'google' | 'github' | 'browser'
|
||||
auth_type?: 'builder_id' | 'identity_center' | 'google' | 'github' | 'browser' | 'device'
|
||||
login_option?: 'google' | 'github' | 'default'
|
||||
redirect_uri?: string
|
||||
proxy_node_id?: string
|
||||
|
||||
@@ -462,6 +462,23 @@ export interface GrokUpstreamMetadata {
|
||||
account_user_id?: string | null
|
||||
}
|
||||
|
||||
export interface XaiUpstreamMetadata {
|
||||
updated_at?: number
|
||||
subscription_title?: string
|
||||
usage_percentage?: number
|
||||
remaining_percentage?: number
|
||||
usage_label?: string
|
||||
usage_limit?: number
|
||||
current_usage?: number
|
||||
remaining?: number
|
||||
next_reset_at?: number
|
||||
prepaid_balance?: number
|
||||
on_demand_cap?: number
|
||||
on_demand_used?: number
|
||||
on_demand_remaining?: number
|
||||
period_type?: string
|
||||
}
|
||||
|
||||
export interface GeminiCliTierMetadata {
|
||||
id?: string | null
|
||||
tierType?: string | null
|
||||
@@ -520,6 +537,7 @@ export interface UpstreamMetadata {
|
||||
chatgpt_web?: ChatGPTWebUpstreamMetadata
|
||||
grok?: GrokUpstreamMetadata
|
||||
gemini_cli?: GeminiCliUpstreamMetadata
|
||||
xai?: XaiUpstreamMetadata
|
||||
}
|
||||
|
||||
// 按格式的健康度数据
|
||||
@@ -758,7 +776,7 @@ export interface HealthRelatedMonitorResponse {
|
||||
related_providers: HealthRelatedMonitor[]
|
||||
}
|
||||
|
||||
export type ProviderType = 'custom' | 'claude_code' | 'codex' | 'chatgpt_web' | 'gemini_cli' | 'antigravity' | 'kiro' | 'grok' | 'windsurf' | 'vertex_ai'
|
||||
export type ProviderType = 'custom' | 'claude_code' | 'codex' | 'chatgpt_web' | 'gemini_cli' | 'antigravity' | 'kiro' | 'grok' | 'xai' | 'windsurf' | 'vertex_ai'
|
||||
|
||||
export interface ClaudeCodeAdvancedConfig {
|
||||
// 会话数量控制:null/undefined 表示不限制
|
||||
|
||||
@@ -334,7 +334,7 @@ const FALLBACK_PRESET_DEFS: PoolPresetMeta[] = [
|
||||
label: 'Free/Team 优先',
|
||||
description: '兼容旧配置:优先消耗 Free、Team 或两者',
|
||||
evidence_hint: '依据 plan_type,保留旧 free_only/team_only/both 语义',
|
||||
providers: ['codex', 'grok', 'kiro', 'windsurf'],
|
||||
providers: ['codex', 'grok', 'kiro', 'windsurf', 'xai'],
|
||||
modes: [
|
||||
{ value: 'free_only', label: 'Free' },
|
||||
{ value: 'team_only', label: 'Team' },
|
||||
@@ -347,7 +347,7 @@ const FALLBACK_PRESET_DEFS: PoolPresetMeta[] = [
|
||||
label: 'Free 优先',
|
||||
description: '优先消耗 Free 账号(依赖 plan_type)',
|
||||
evidence_hint: '依据 plan_type(Free 账号优先调度)',
|
||||
providers: ['codex', 'grok', 'kiro', 'windsurf'],
|
||||
providers: ['codex', 'grok', 'kiro', 'windsurf', 'xai'],
|
||||
modes: null,
|
||||
default_mode: null,
|
||||
},
|
||||
@@ -356,7 +356,7 @@ const FALLBACK_PRESET_DEFS: PoolPresetMeta[] = [
|
||||
label: 'Team 优先',
|
||||
description: '优先消耗 Team 账号(依赖 plan_type)',
|
||||
evidence_hint: '依据 plan_type(Team 账号优先调度)',
|
||||
providers: ['codex', 'grok', 'kiro', 'windsurf'],
|
||||
providers: ['codex', 'grok', 'kiro', 'windsurf', 'xai'],
|
||||
modes: null,
|
||||
default_mode: null,
|
||||
},
|
||||
@@ -365,7 +365,7 @@ const FALLBACK_PRESET_DEFS: PoolPresetMeta[] = [
|
||||
label: 'Plus 优先',
|
||||
description: '优先消耗 Plus 账号(依赖 plan_type)',
|
||||
evidence_hint: '依据 plan_type(Plus 账号优先调度)',
|
||||
providers: ['codex', 'grok', 'kiro', 'windsurf'],
|
||||
providers: ['codex', 'grok', 'kiro', 'windsurf', 'xai'],
|
||||
modes: null,
|
||||
default_mode: null,
|
||||
},
|
||||
@@ -374,7 +374,7 @@ const FALLBACK_PRESET_DEFS: PoolPresetMeta[] = [
|
||||
label: 'Pro 优先',
|
||||
description: '优先消耗 Pro 账号(依赖 plan_type)',
|
||||
evidence_hint: '依据 plan_type(Pro 账号优先调度)',
|
||||
providers: ['codex', 'grok', 'kiro', 'windsurf'],
|
||||
providers: ['codex', 'grok', 'kiro', 'windsurf', 'xai'],
|
||||
modes: null,
|
||||
default_mode: null,
|
||||
},
|
||||
@@ -392,7 +392,7 @@ const FALLBACK_PRESET_DEFS: PoolPresetMeta[] = [
|
||||
label: '额度刷新优先',
|
||||
description: '优先选即将刷新额度的账号',
|
||||
evidence_hint: '依据账号额度重置倒计时(next_reset / reset_seconds)',
|
||||
providers: ['codex', 'grok', 'kiro', 'windsurf'],
|
||||
providers: ['codex', 'grok', 'kiro', 'windsurf', 'xai'],
|
||||
default_enabled_providers: ['codex', 'windsurf'],
|
||||
modes: null,
|
||||
default_mode: null,
|
||||
|
||||
@@ -244,6 +244,129 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- xAI: 设备授权 -->
|
||||
<template v-else-if="isXaiProvider">
|
||||
<div class="space-y-3">
|
||||
<div class="h-[265px]">
|
||||
<div
|
||||
v-if="device.status === 'error' || device.status === 'expired'"
|
||||
class="rounded-xl border border-destructive/20 bg-destructive/5 p-5"
|
||||
>
|
||||
<div class="flex flex-col items-center text-center space-y-3">
|
||||
<div class="w-10 h-10 rounded-full bg-destructive/10 flex items-center justify-center">
|
||||
<AlertCircle class="w-5 h-5 text-destructive" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-medium text-destructive">
|
||||
{{ legacyT(device.status === 'expired' ? '授权已过期' : '授权失败') }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ legacyT(device.error || '请重试') }}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@click="resetDevice"
|
||||
>
|
||||
{{ legacyT('重新开始') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="device.starting && !device.session_id"
|
||||
class="flex items-center justify-center py-12"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mx-auto mb-3" />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ legacyT('正在准备设备授权...') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="device.session_id && device.status === 'pending'"
|
||||
class="rounded-xl border border-border bg-muted/20 p-5"
|
||||
>
|
||||
<div class="flex flex-col items-center text-center space-y-4">
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 rounded-full bg-primary/20 animate-ping" />
|
||||
<div class="relative w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<ExternalLink class="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-medium">
|
||||
{{ legacyT('在浏览器中输入设备码完成授权') }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ legacyT('授权完成后此页面将自动更新') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="device.user_code"
|
||||
class="flex items-center gap-2 rounded-lg border border-border bg-background px-3 py-2"
|
||||
>
|
||||
<span class="text-lg font-mono font-bold tracking-[0.2em]">{{ device.user_code }}</span>
|
||||
<button
|
||||
class="p-1 rounded hover:bg-muted transition-colors"
|
||||
:title="legacyT('复制设备码')"
|
||||
@click="copyToClipboard(device.user_code)"
|
||||
>
|
||||
<Copy class="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<div class="animate-spin rounded-full h-3 w-3 border-[1.5px] border-primary/30 border-t-primary" />
|
||||
<span>{{ remainingText }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 w-full">
|
||||
<Button
|
||||
class="flex-1"
|
||||
size="sm"
|
||||
:disabled="!device.verification_uri_complete && !device.verification_uri"
|
||||
@click="openDeviceVerificationUrl"
|
||||
>
|
||||
<ExternalLink class="w-3.5 h-3.5 mr-1.5" />
|
||||
{{ legacyT('打开授权页面') }}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
:disabled="!device.verification_uri_complete && !device.verification_uri"
|
||||
@click="copyToClipboard(device.verification_uri_complete || device.verification_uri)"
|
||||
>
|
||||
<Copy class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="flex h-full flex-col items-center justify-center gap-3"
|
||||
>
|
||||
<p class="text-xs text-muted-foreground text-center">
|
||||
{{ legacyT('使用 xAI 设备授权登录 Grok CLI,或改为导入 API Key / Refresh Token。') }}
|
||||
</p>
|
||||
<Button
|
||||
class="w-full"
|
||||
:disabled="device.starting"
|
||||
@click="startDeviceAuth"
|
||||
>
|
||||
{{ device.starting ? legacyT('正在准备授权...') : legacyT('开始授权') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Kiro: 设备授权模式 -->
|
||||
<template v-else-if="isKiroProvider">
|
||||
<div class="space-y-3">
|
||||
@@ -994,7 +1117,7 @@ let oauthInitRequestId = 0
|
||||
let oauthCompleteRequestId = 0
|
||||
|
||||
// 设备授权状态
|
||||
type DeviceAuthType = 'default' | 'google' | 'github' | 'builder_id' | 'identity_center'
|
||||
type DeviceAuthType = 'default' | 'google' | 'github' | 'builder_id' | 'identity_center' | 'device'
|
||||
type WindsurfLoginOption = 'default' | 'google' | 'github'
|
||||
|
||||
interface DeviceAuthState {
|
||||
@@ -1075,9 +1198,10 @@ const isOpen = computed(() => props.open)
|
||||
const isKiroProvider = computed(() => (props.providerType || '').toLowerCase() === 'kiro')
|
||||
const isGrokProvider = computed(() => (props.providerType || '').toLowerCase() === 'grok')
|
||||
const isWindsurfProvider = computed(() => (props.providerType || '').toLowerCase() === 'windsurf')
|
||||
const isXaiProvider = computed(() => (props.providerType || '').toLowerCase() === 'xai')
|
||||
const isCodexProvider = computed(() => (props.providerType || '').toLowerCase() === 'codex')
|
||||
const isClaudeCodeProvider = computed(() => (props.providerType || '').toLowerCase() === 'claude_code')
|
||||
const isDeviceBrowserProvider = computed(() => isKiroProvider.value || isWindsurfProvider.value)
|
||||
const isDeviceBrowserProvider = computed(() => isKiroProvider.value || isWindsurfProvider.value || isXaiProvider.value)
|
||||
const showAuthorizationMode = computed(() => !isGrokProvider.value)
|
||||
const defaultMode = computed<DialogMode>(() => (isGrokProvider.value ? 'import' : 'oauth'))
|
||||
|
||||
@@ -1101,7 +1225,7 @@ const isManualDeviceCallbackPending = computed(() =>
|
||||
|
||||
const authorizationModeLabel = computed(() => {
|
||||
if (isWindsurfProvider.value) return legacyT('浏览器登录')
|
||||
if (isDeviceBrowserProvider.value) return legacyT('设备授权')
|
||||
if (isXaiProvider.value || isDeviceBrowserProvider.value) return legacyT('设备授权')
|
||||
return legacyT('获取授权')
|
||||
})
|
||||
|
||||
@@ -1215,6 +1339,9 @@ const importManualPlaceholder = computed(() => {
|
||||
if (isClaudeCodeProvider.value) {
|
||||
return legacyT('粘贴 Claude Refresh Token 或 Claude Code .credentials.json 内容')
|
||||
}
|
||||
if (isXaiProvider.value) {
|
||||
return legacyT('粘贴 xAI API Key、Access Token,或包含 refresh_token / api_key 的 JSON')
|
||||
}
|
||||
if (isWindsurfProvider.value) {
|
||||
return legacyT('粘贴 show-auth-token Token、API key 或 JSON 内容')
|
||||
}
|
||||
@@ -1477,11 +1604,17 @@ function resetDevice() {
|
||||
totp.stop()
|
||||
const { auth_type, start_url, region, totp_secret } = device.value
|
||||
device.value = createInitialDeviceState()
|
||||
device.value.auth_type = isWindsurfProvider.value ? (auth_type === 'google' || auth_type === 'github' ? auth_type : 'default') : auth_type
|
||||
device.value.auth_type = isXaiProvider.value
|
||||
? 'device'
|
||||
: isWindsurfProvider.value
|
||||
? (auth_type === 'google' || auth_type === 'github' ? auth_type : 'default')
|
||||
: auth_type
|
||||
device.value.start_url = start_url
|
||||
device.value.region = region
|
||||
device.value.totp_secret = totp_secret
|
||||
if (!isWindsurfProvider.value && (device.value.auth_type === 'google' || device.value.auth_type === 'github')) {
|
||||
if (isXaiProvider.value) {
|
||||
void ensureXaiDeviceAuth()
|
||||
} else if (!isWindsurfProvider.value && (device.value.auth_type === 'google' || device.value.auth_type === 'github')) {
|
||||
void ensureKiroSocialDeviceAuth()
|
||||
}
|
||||
}
|
||||
@@ -1500,6 +1633,8 @@ function resetForm() {
|
||||
device.value = createInitialDeviceState()
|
||||
if (isWindsurfProvider.value) {
|
||||
device.value.auth_type = 'default'
|
||||
} else if (isXaiProvider.value) {
|
||||
device.value.auth_type = 'device'
|
||||
}
|
||||
importText.value = ''
|
||||
importing.value = false
|
||||
@@ -1531,6 +1666,8 @@ function switchMode(newMode: DialogMode) {
|
||||
if (newMode === 'oauth') {
|
||||
if (isKiroProvider.value) {
|
||||
void ensureKiroSocialDeviceAuth()
|
||||
} else if (isXaiProvider.value) {
|
||||
void ensureXaiDeviceAuth()
|
||||
} else if (!oauth.value.authorization_url && !oauth.value.starting) {
|
||||
initOAuth()
|
||||
}
|
||||
@@ -1833,6 +1970,29 @@ function parseImportText(text: string): {
|
||||
return { refresh_token: trimmed }
|
||||
}
|
||||
|
||||
if (isXaiProvider.value) {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(trimmed)
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
const obj = parsed as Record<string, unknown>
|
||||
const apiKey = normalizeStringField(obj.api_key) ?? normalizeStringField(obj.apiKey)
|
||||
const refreshToken = normalizeStringField(obj.refresh_token) ?? normalizeStringField(obj.refreshToken)
|
||||
const accessToken = normalizeStringField(obj.access_token) ?? normalizeStringField(obj.accessToken) ?? apiKey
|
||||
if (refreshToken || accessToken) {
|
||||
return {
|
||||
refresh_token: refreshToken,
|
||||
access_token: accessToken,
|
||||
name: normalizeStringField(obj.name) ?? normalizeStringField(obj.email),
|
||||
email: normalizeStringField(obj.email),
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Raw xAI API keys / access tokens are imported as access_token.
|
||||
}
|
||||
return { access_token: trimmed }
|
||||
}
|
||||
|
||||
if (isGrokProvider.value) {
|
||||
const cookieImport = parseGrokCookieImport(trimmed)
|
||||
if (cookieImport) {
|
||||
@@ -2367,17 +2527,20 @@ async function startDeviceAuth() {
|
||||
device.value.error = ''
|
||||
try {
|
||||
const isWindsurf = isWindsurfProvider.value
|
||||
const isXai = isXaiProvider.value
|
||||
const isBuilderID = requestedAuthType === 'builder_id'
|
||||
const isSocial = requestedAuthType === 'google' || requestedAuthType === 'github'
|
||||
const isSocial = !isXai && (requestedAuthType === 'google' || requestedAuthType === 'github')
|
||||
const windsurfLoginOption: WindsurfLoginOption = isSocial ? requestedAuthType : 'default'
|
||||
const authTypeForRequest = isWindsurf
|
||||
? 'browser'
|
||||
: (requestedAuthType === 'default' ? 'google' : requestedAuthType)
|
||||
: isXai
|
||||
? 'device'
|
||||
: (requestedAuthType === 'default' ? 'google' : requestedAuthType)
|
||||
const resp = await startDeviceAuthorize(props.providerId, {
|
||||
auth_type: authTypeForRequest,
|
||||
login_option: isWindsurf ? windsurfLoginOption : undefined,
|
||||
start_url: isWindsurf ? undefined : (isBuilderID ? BUILDER_ID_START_URL : (isSocial ? undefined : (device.value.start_url.trim() || undefined))),
|
||||
region: isWindsurf ? undefined : (isBuilderID || isSocial ? BUILDER_ID_REGION : (device.value.region.trim() || undefined)),
|
||||
start_url: (isWindsurf || isXai) ? undefined : (isBuilderID ? BUILDER_ID_START_URL : (isSocial ? undefined : (device.value.start_url.trim() || undefined))),
|
||||
region: (isWindsurf || isXai) ? undefined : (isBuilderID || isSocial ? BUILDER_ID_REGION : (device.value.region.trim() || undefined)),
|
||||
proxy_node_id: selectedProxyNodeId.value || undefined,
|
||||
})
|
||||
if (requestId !== deviceAuthRequestId || device.value.auth_type !== requestedAuthType) return
|
||||
@@ -2417,6 +2580,13 @@ async function ensureKiroSocialDeviceAuth() {
|
||||
await startDeviceAuth()
|
||||
}
|
||||
|
||||
async function ensureXaiDeviceAuth() {
|
||||
if (!props.open || !props.providerId || !isXaiProvider.value) return
|
||||
if (device.value.starting) return
|
||||
if (device.value.session_id && (device.value.status === 'pending' || device.value.status === 'authorized')) return
|
||||
await startDeviceAuth()
|
||||
}
|
||||
|
||||
function scheduleDevicePoll() {
|
||||
if (devicePollTimer) clearTimeout(devicePollTimer)
|
||||
devicePollTimer = setTimeout(() => pollDevice(), device.value.interval * 1000)
|
||||
@@ -2525,6 +2695,9 @@ watch(
|
||||
}
|
||||
if (isWindsurfProvider.value) {
|
||||
device.value.auth_type = 'default'
|
||||
} else if (isXaiProvider.value) {
|
||||
device.value.auth_type = 'device'
|
||||
void ensureXaiDeviceAuth()
|
||||
} else if (isKiroProvider.value) {
|
||||
void ensureKiroSocialDeviceAuth()
|
||||
} else {
|
||||
@@ -2554,6 +2727,9 @@ watch(
|
||||
device.value.auth_type = ['default', 'google', 'github'].includes(device.value.auth_type)
|
||||
? device.value.auth_type
|
||||
: 'default'
|
||||
} else if (props.open && isXaiProvider.value && mode.value === 'oauth') {
|
||||
device.value.auth_type = 'device'
|
||||
void ensureXaiDeviceAuth()
|
||||
} else if (props.open && isKiroProvider.value && mode.value === 'oauth') {
|
||||
void ensureKiroSocialDeviceAuth()
|
||||
}
|
||||
|
||||
@@ -514,6 +514,65 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- xAI / Grok Build 订阅额度 -->
|
||||
<div
|
||||
v-if="provider.provider_type === 'xai' && hasXaiQuotaDisplayData(key)"
|
||||
class="mt-2 p-2 rounded-md bg-muted/30"
|
||||
>
|
||||
<ProviderQuotaSectionHeader
|
||||
:title="legacyT('账号配额')"
|
||||
:loading="refreshingQuota"
|
||||
:updated-text="getXaiQuotaDisplay(key)?.updated_at ? formatKiroUpdatedAt(getXaiQuotaDisplay(key)?.updated_at || 0) : null"
|
||||
/>
|
||||
<div class="space-y-2">
|
||||
<ProviderQuotaProgressRow
|
||||
v-if="getXaiQuotaDisplay(key)?.usage_percentage !== undefined || getXaiQuotaDisplay(key)?.remaining_percentage !== undefined"
|
||||
:label="legacyT(getXaiUsageLabel(key))"
|
||||
:used-percent="getXaiUsedPercent(key)"
|
||||
:remaining-percent="getXaiRemainingPercent(key)"
|
||||
:meter-class="getQuotaRemainingClass(getXaiUsedPercent(key))"
|
||||
:bar-class="getQuotaRemainingBarColor(getXaiUsedPercent(key))"
|
||||
:reset-text="getXaiQuotaDisplay(key)?.next_reset_at
|
||||
? `${formatKiroResetTime(getXaiQuotaDisplay(key)?.next_reset_at)}${legacyT('重置')}`
|
||||
: null"
|
||||
>
|
||||
<template
|
||||
v-if="getXaiQuotaDisplay(key)?.usage_limit != null"
|
||||
#footer
|
||||
>
|
||||
<div class="flex items-center justify-between text-[9px] text-muted-foreground/70 mt-0.5">
|
||||
<span>
|
||||
{{ formatKiroUsage(getXaiQuotaDisplay(key)?.current_usage) }} /
|
||||
{{ formatKiroUsage(getXaiQuotaDisplay(key)?.usage_limit) }}
|
||||
</span>
|
||||
<span v-if="getXaiQuotaDisplay(key)?.next_reset_at">
|
||||
{{ formatKiroResetTime(getXaiQuotaDisplay(key)?.next_reset_at) }}{{ legacyT('重置') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</ProviderQuotaProgressRow>
|
||||
<div
|
||||
v-if="getXaiQuotaDisplay(key)?.prepaid_balance != null"
|
||||
class="text-[10px] text-muted-foreground"
|
||||
>
|
||||
{{ legacyT('预付额度') }}: {{ formatKiroUsage(getXaiQuotaDisplay(key)?.prepaid_balance) }}
|
||||
</div>
|
||||
<ProviderQuotaProgressRow
|
||||
v-if="getXaiQuotaDisplay(key)?.on_demand_cap"
|
||||
:label="legacyT('按需额度')"
|
||||
:used-percent="getXaiOnDemandUsedPercent(key)"
|
||||
:meter-class="getQuotaRemainingClass(getXaiOnDemandUsedPercent(key))"
|
||||
:bar-class="getQuotaRemainingBarColor(getXaiOnDemandUsedPercent(key))"
|
||||
>
|
||||
<template #footer>
|
||||
<div class="text-[9px] text-muted-foreground/70 mt-0.5">
|
||||
{{ formatKiroUsage(getXaiQuotaDisplay(key)?.on_demand_used) }} /
|
||||
{{ formatKiroUsage(getXaiQuotaDisplay(key)?.on_demand_cap) }}
|
||||
</div>
|
||||
</template>
|
||||
</ProviderQuotaProgressRow>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Windsurf 上游额度信息 -->
|
||||
<div
|
||||
v-if="provider.provider_type === 'windsurf' && (hasWindsurfQuotaDisplayData(key) || isWindsurfUnavailableKey(key) || isWindsurfExhaustedKey(key))"
|
||||
@@ -1005,6 +1064,7 @@ import type {
|
||||
GrokUpstreamMetadata,
|
||||
KiroUpstreamMetadata,
|
||||
WindsurfUpstreamMetadata,
|
||||
XaiUpstreamMetadata,
|
||||
QuotaResetCreditsSnapshot,
|
||||
QuotaStatusSnapshot,
|
||||
QuotaWindowSnapshot,
|
||||
@@ -1858,7 +1918,7 @@ function quotaSnapshotHasDisplayData(quota: QuotaStatusSnapshot | null | undefin
|
||||
|
||||
function getQuotaSnapshotForProvider(
|
||||
key: EndpointAPIKey,
|
||||
providerType: 'codex' | 'kiro' | 'windsurf' | 'antigravity' | 'chatgpt_web' | 'gemini_cli' | 'grok',
|
||||
providerType: 'codex' | 'kiro' | 'windsurf' | 'antigravity' | 'chatgpt_web' | 'gemini_cli' | 'grok' | 'xai',
|
||||
): QuotaStatusSnapshot | null {
|
||||
const quota = key.status_snapshot?.quota
|
||||
if (!quota) return null
|
||||
@@ -2179,6 +2239,90 @@ function hasKiroQuotaDisplayData(key: EndpointAPIKey): boolean {
|
||||
return !!kiro && (kiro.usage_percentage !== undefined || kiro.usage_limit !== undefined)
|
||||
}
|
||||
|
||||
function getXaiQuotaDisplay(key: EndpointAPIKey): XaiUpstreamMetadata | null {
|
||||
const quota = getQuotaSnapshotForProvider(key, 'xai')
|
||||
if (!quota) return null
|
||||
|
||||
const display: XaiUpstreamMetadata = {}
|
||||
const updatedAt = getQuotaSnapshotUpdatedAt(quota)
|
||||
if (updatedAt !== undefined) display.updated_at = updatedAt
|
||||
if (quota.plan_type) display.subscription_title = quota.plan_type
|
||||
|
||||
const usageWindow =
|
||||
getQuotaWindow(quota, 'usage')
|
||||
?? getQuotaWindowByScope(quota, 'account')[0]
|
||||
?? null
|
||||
if (usageWindow) {
|
||||
const usedPercent = getQuotaWindowUsedPercent(usageWindow)
|
||||
const remainingPercent = getQuotaWindowRemainingPercent(usageWindow)
|
||||
if (usedPercent !== undefined) display.usage_percentage = usedPercent
|
||||
if (remainingPercent !== undefined) display.remaining_percentage = remainingPercent
|
||||
const usageLabel = String(usageWindow.label || '').trim()
|
||||
if (usageLabel) display.usage_label = usageLabel
|
||||
if (typeof usageWindow.used_value === 'number') display.current_usage = usageWindow.used_value
|
||||
if (typeof usageWindow.limit_value === 'number') display.usage_limit = usageWindow.limit_value
|
||||
if (typeof usageWindow.remaining_value === 'number') display.remaining = usageWindow.remaining_value
|
||||
const nextResetAt =
|
||||
getQuotaWindowResetAt(usageWindow)
|
||||
?? (() => {
|
||||
const resetSeconds = getQuotaWindowResetSeconds(usageWindow)
|
||||
if (updatedAt === undefined || resetSeconds === undefined) return undefined
|
||||
return updatedAt + resetSeconds
|
||||
})()
|
||||
if (nextResetAt !== undefined) display.next_reset_at = nextResetAt
|
||||
}
|
||||
|
||||
const prepaidWindow = getQuotaWindow(quota, 'prepaid')
|
||||
if (typeof prepaidWindow?.remaining_value === 'number') {
|
||||
display.prepaid_balance = prepaidWindow.remaining_value
|
||||
}
|
||||
|
||||
const onDemandWindow = getQuotaWindow(quota, 'on_demand')
|
||||
if (typeof onDemandWindow?.limit_value === 'number') display.on_demand_cap = onDemandWindow.limit_value
|
||||
if (typeof onDemandWindow?.used_value === 'number') display.on_demand_used = onDemandWindow.used_value
|
||||
if (typeof onDemandWindow?.remaining_value === 'number') display.on_demand_remaining = onDemandWindow.remaining_value
|
||||
|
||||
return Object.keys(display).length > 0 ? display : null
|
||||
}
|
||||
|
||||
function hasXaiQuotaDisplayData(key: EndpointAPIKey): boolean {
|
||||
const xai = getXaiQuotaDisplay(key)
|
||||
return !!xai && (
|
||||
xai.usage_percentage !== undefined
|
||||
|| xai.remaining_percentage !== undefined
|
||||
|| xai.prepaid_balance !== undefined
|
||||
|| xai.on_demand_cap !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
function getXaiUsageLabel(key: EndpointAPIKey): string {
|
||||
const display = getXaiQuotaDisplay(key)
|
||||
if (display?.usage_label) return display.usage_label
|
||||
const title = display?.subscription_title
|
||||
return title ? `使用额度 (${title})` : '使用额度'
|
||||
}
|
||||
|
||||
function getXaiUsedPercent(key: EndpointAPIKey): number {
|
||||
return Math.min(Math.max(100 - getXaiRemainingPercent(key), 0), 100)
|
||||
}
|
||||
|
||||
function getXaiRemainingPercent(key: EndpointAPIKey): number {
|
||||
const xai = getXaiQuotaDisplay(key)
|
||||
if (xai?.remaining_percentage != null && Number.isFinite(xai.remaining_percentage)) {
|
||||
return Math.min(Math.max(xai.remaining_percentage, 0), 100)
|
||||
}
|
||||
if (xai?.usage_percentage != null && Number.isFinite(xai.usage_percentage)) {
|
||||
return Math.min(Math.max(100 - xai.usage_percentage, 0), 100)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function getXaiOnDemandUsedPercent(key: EndpointAPIKey): number {
|
||||
const xai = getXaiQuotaDisplay(key)
|
||||
if (!xai?.on_demand_cap || xai.on_demand_cap <= 0) return 0
|
||||
return Math.max(Math.min(((xai.on_demand_used || 0) / xai.on_demand_cap) * 100, 100), 0)
|
||||
}
|
||||
|
||||
type GrokQuotaDisplay = GrokUpstreamMetadata & {
|
||||
usage_percentage?: number
|
||||
usage_limit?: number
|
||||
@@ -2696,6 +2840,28 @@ function shouldAutoRefreshGrokQuota(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
function shouldAutoRefreshXaiQuota(): boolean {
|
||||
if (provider.value?.provider_type !== 'xai') return false
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
|
||||
for (const { key } of allKeys.value) {
|
||||
if (!key.is_active) continue
|
||||
|
||||
if (isTokenExpiringSoon(key, now)) return true
|
||||
|
||||
if (!hasXaiQuotaDisplayData(key)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const updatedAt = getXaiQuotaDisplay(key)?.updated_at
|
||||
if (typeof updatedAt !== 'number' || (now - updatedAt) > AUTO_QUOTA_REFRESH_STALE_SECONDS) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function shouldAutoRefreshWindsurfQuota(): boolean {
|
||||
if (provider.value?.provider_type !== 'windsurf') return false
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
@@ -2824,7 +2990,7 @@ async function autoRefreshQuotaInBackground(): Promise<boolean> {
|
||||
if (refreshingQuota.value) return false
|
||||
|
||||
const providerType = provider.value?.provider_type
|
||||
if (providerType !== 'codex' && providerType !== 'gemini_cli' && providerType !== 'antigravity' && providerType !== 'kiro' && providerType !== 'windsurf' && providerType !== 'chatgpt_web' && providerType !== 'grok') return false
|
||||
if (providerType !== 'codex' && providerType !== 'gemini_cli' && providerType !== 'antigravity' && providerType !== 'kiro' && providerType !== 'windsurf' && providerType !== 'chatgpt_web' && providerType !== 'grok' && providerType !== 'xai') return false
|
||||
|
||||
// 检查是否需要刷新
|
||||
let shouldRefresh = false
|
||||
@@ -2838,6 +3004,8 @@ async function autoRefreshQuotaInBackground(): Promise<boolean> {
|
||||
shouldRefresh = shouldAutoRefreshKiroQuota()
|
||||
} else if (providerType === 'grok') {
|
||||
shouldRefresh = shouldAutoRefreshGrokQuota()
|
||||
} else if (providerType === 'xai') {
|
||||
shouldRefresh = shouldAutoRefreshXaiQuota()
|
||||
} else if (providerType === 'windsurf') {
|
||||
shouldRefresh = shouldAutoRefreshWindsurfQuota()
|
||||
} else if (providerType === 'chatgpt_web') {
|
||||
@@ -2856,6 +3024,8 @@ async function autoRefreshQuotaInBackground(): Promise<boolean> {
|
||||
hadCachedQuota = allKeys.value.some(({ key }) => key.is_active && hasKiroQuotaDisplayData(key))
|
||||
} else if (providerType === 'grok') {
|
||||
hadCachedQuota = allKeys.value.some(({ key }) => key.is_active && hasGrokQuotaDisplayData(key))
|
||||
} else if (providerType === 'xai') {
|
||||
hadCachedQuota = allKeys.value.some(({ key }) => key.is_active && hasXaiQuotaDisplayData(key))
|
||||
} else if (providerType === 'windsurf') {
|
||||
hadCachedQuota = allKeys.value.some(({ key }) => key.is_active && hasWindsurfQuotaDisplayData(key))
|
||||
} else if (providerType === 'chatgpt_web') {
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
<SelectItem value="grok">
|
||||
Grok
|
||||
</SelectItem>
|
||||
<SelectItem value="xai">
|
||||
xAI
|
||||
</SelectItem>
|
||||
<SelectItem value="kiro">
|
||||
Kiro
|
||||
</SelectItem>
|
||||
@@ -93,6 +96,9 @@
|
||||
<SelectItem value="grok">
|
||||
Grok
|
||||
</SelectItem>
|
||||
<SelectItem value="xai">
|
||||
xAI
|
||||
</SelectItem>
|
||||
<SelectItem value="kiro">
|
||||
Kiro
|
||||
</SelectItem>
|
||||
|
||||
@@ -54,6 +54,24 @@ describe('provider quota display components', () => {
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('fills the remaining bar even when used percent is zero', () => {
|
||||
const { root, unmount } = mount(ProviderQuotaProgressRow, {
|
||||
label: '周额度',
|
||||
usedPercent: 0,
|
||||
remainingPercent: 86,
|
||||
meterClass: 'text-green-600',
|
||||
barClass: 'bg-green-500',
|
||||
resetText: '5天0小时后重置',
|
||||
})
|
||||
|
||||
expect(root.querySelector('[data-testid="provider-quota-progress-meter"]')?.textContent?.trim()).toBe('86.0%')
|
||||
expect((root.querySelector('[data-testid="provider-quota-progress-bar"]') as HTMLElement).style.width).toBe('86%')
|
||||
expect(root.textContent).toContain('周额度')
|
||||
expect(root.querySelector('[data-testid="provider-quota-progress-reset"]')?.textContent).toBe('5天0小时后重置')
|
||||
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('renders section loading and updated state', () => {
|
||||
const Probe = defineComponent({
|
||||
setup() {
|
||||
|
||||
@@ -1386,6 +1386,7 @@ function formatAuthType(authType: string): string {
|
||||
if (lowered === 'antigravity') return 'Antigravity OAuth'
|
||||
if (lowered === 'kiro') return 'Kiro OAuth'
|
||||
if (lowered === 'grok') return 'Grok OAuth'
|
||||
if (lowered === 'xai') return 'xAI OAuth'
|
||||
return authType
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +39,12 @@ const MODEL_TEST_OAUTH_INHERITS_PROVIDER_FORMATS = new Set([
|
||||
'vertex_ai',
|
||||
'antigravity',
|
||||
'kiro',
|
||||
'xai',
|
||||
])
|
||||
|
||||
const MODEL_TEST_BEARER_INHERITS_PROVIDER_FORMATS = new Set([
|
||||
'chatgpt_web',
|
||||
'xai',
|
||||
])
|
||||
|
||||
const MODEL_TEST_DIAGNOSTIC_LABELS: Record<string, string> = {
|
||||
|
||||
@@ -16,6 +16,12 @@ describe('providerTypeUtils', () => {
|
||||
expect(isKeyManagedProviderType('grok')).toBe(false)
|
||||
})
|
||||
|
||||
it('treats xAI as an OAuth account provider', () => {
|
||||
expect(isOAuthAccountProviderType('xai')).toBe(true)
|
||||
expect(isOAuthAccountProviderType('xAI')).toBe(true)
|
||||
expect(isKeyManagedProviderType('xai')).toBe(false)
|
||||
})
|
||||
|
||||
it('treats Windsurf as an OAuth account provider', () => {
|
||||
expect(isOAuthAccountProviderType('windsurf')).toBe(true)
|
||||
expect(isOAuthAccountProviderType('Windsurf')).toBe(true)
|
||||
|
||||
@@ -12,6 +12,7 @@ const oauthAccountProviderTypes = new Set([
|
||||
'antigravity',
|
||||
'kiro',
|
||||
'grok',
|
||||
'xai',
|
||||
'windsurf',
|
||||
])
|
||||
|
||||
|
||||
@@ -2146,7 +2146,10 @@ const legacyExactEnglishMessages: Record<string, string> = {
|
||||
'账号不可用': 'Account unavailable',
|
||||
'日额度': 'Daily quota',
|
||||
'周额度': 'Weekly quota',
|
||||
'月额度': 'Monthly quota',
|
||||
'剩余额度': 'Remaining quota',
|
||||
'预付额度': 'Prepaid credits',
|
||||
'按需额度': 'On-demand credits',
|
||||
'点击编辑优先级': 'Edit priority',
|
||||
'点击编辑倍率': 'Edit multiplier',
|
||||
'同步失败': 'Sync failed',
|
||||
|
||||
@@ -359,4 +359,30 @@ describe('providerKeyQuota', () => {
|
||||
},
|
||||
}, 'windsurf')).toBe('可用模型 3 个')
|
||||
})
|
||||
|
||||
it('formats xAI weekly credits as remaining percent', () => {
|
||||
expect(getQuotaDisplayText({
|
||||
status_snapshot: {
|
||||
oauth: { code: 'valid' },
|
||||
account: { code: 'ok', blocked: false },
|
||||
quota: {
|
||||
provider_type: 'xai',
|
||||
code: 'ok',
|
||||
exhausted: false,
|
||||
windows: [
|
||||
{
|
||||
code: 'usage',
|
||||
scope: 'account',
|
||||
used_ratio: 0.46,
|
||||
remaining_ratio: 0.54,
|
||||
},
|
||||
{
|
||||
code: 'prepaid',
|
||||
remaining_value: 12.5,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}, 'xai')).toBe('剩余 54.0% | 预付剩余 12.5')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ export const OAUTH_ICONS: Record<string, string> = {
|
||||
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>`,
|
||||
xai: `<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
|
||||
|
||||
@@ -265,6 +265,29 @@ function getKiroQuotaText(quota: QuotaStatusSnapshot): string | null {
|
||||
return normalizeText(quota.label)
|
||||
}
|
||||
|
||||
function getXaiQuotaText(quota: QuotaStatusSnapshot): string | null {
|
||||
const parts: string[] = []
|
||||
const usageText = getKiroQuotaText(quota)
|
||||
if (usageText) parts.push(usageText)
|
||||
|
||||
const prepaid = getQuotaWindow(quota, 'prepaid')
|
||||
if (typeof prepaid?.remaining_value === 'number') {
|
||||
parts.push(`预付剩余 ${formatQuotaValue(prepaid.remaining_value)}`)
|
||||
}
|
||||
|
||||
const onDemand = getQuotaWindow(quota, 'on_demand')
|
||||
const onDemandRemaining = getQuotaWindowRemainingPercent(onDemand)
|
||||
if (onDemandRemaining != null) {
|
||||
const valueText = getQuotaWindowValueText(onDemand)
|
||||
parts.push(`按需剩余 ${formatPercent(onDemandRemaining)}${valueText ? ` (${valueText})` : ''}`)
|
||||
} else if (typeof onDemand?.remaining_value === 'number') {
|
||||
parts.push(`按需剩余 ${formatQuotaValue(onDemand.remaining_value)}`)
|
||||
}
|
||||
|
||||
if (parts.length > 0) return parts.join(' | ')
|
||||
return normalizeText(quota.label)
|
||||
}
|
||||
|
||||
function getGrokQuotaText(quota: QuotaStatusSnapshot): string | null {
|
||||
const code = normalizeText(quota.code)?.toLowerCase()
|
||||
if (code === 'banned') {
|
||||
@@ -452,6 +475,8 @@ export function getQuotaSnapshotFallbackText(
|
||||
return getCodexQuotaText(quota)
|
||||
case 'kiro':
|
||||
return getKiroQuotaText(quota)
|
||||
case 'xai':
|
||||
return getXaiQuotaText(quota)
|
||||
case 'grok':
|
||||
return getGrokQuotaText(quota)
|
||||
case 'windsurf':
|
||||
|
||||
@@ -1700,6 +1700,7 @@ const showAccountQuotaColumn = computed(() => {
|
||||
|| selectedProviderType.value === 'antigravity'
|
||||
|| selectedProviderType.value === 'grok'
|
||||
|| selectedProviderType.value === 'chatgpt_web'
|
||||
|| selectedProviderType.value === 'xai'
|
||||
})
|
||||
|
||||
const desktopColumnWidths = computed(() => {
|
||||
@@ -2149,6 +2150,7 @@ const quotaRefreshSupported = computed(() => {
|
||||
|| selectedProviderType.value === 'antigravity'
|
||||
|| selectedProviderType.value === 'grok'
|
||||
|| selectedProviderType.value === 'chatgpt_web'
|
||||
|| selectedProviderType.value === 'xai'
|
||||
})
|
||||
|
||||
function canResetCycleStats(_key: PoolKeyDetail): boolean {
|
||||
@@ -3480,8 +3482,10 @@ function normalizeQuotaLabel(label: string): string {
|
||||
if (/spark/i.test(normalized) && normalized.includes('周')) return 'Spark周'
|
||||
if (normalized.includes('5H')) return '5H'
|
||||
if (normalized.includes('周')) return '周'
|
||||
if (normalized.includes('月')) return '月'
|
||||
if (normalized.includes('最低剩余')) return '最低'
|
||||
if (normalized === '剩余' || normalized.includes('剩余')) return '剩余'
|
||||
if (normalized === '额度') return '额度'
|
||||
return normalized
|
||||
}
|
||||
|
||||
@@ -3490,6 +3494,9 @@ function getQuotaProgressLabel(label: string): string {
|
||||
if (label === '5H') return '5H'
|
||||
if (label === '周') return '周'
|
||||
if (label === '月') return '月'
|
||||
if (label === '周额度') return '周'
|
||||
if (label === '月额度') return '月'
|
||||
if (label === '额度') return '额度'
|
||||
if (label === 'Spark5H') return 'Spark5H'
|
||||
if (label === 'Spark周') return 'Spark周'
|
||||
if (label === '最低') return '最低'
|
||||
@@ -3498,7 +3505,7 @@ function getQuotaProgressLabel(label: string): string {
|
||||
}
|
||||
|
||||
function getQuotaProgressCountdown(item: QuotaProgressItem) {
|
||||
const staticResetLabels = ['日', '5H', '周', '月', 'Spark5H', 'Spark周', 'Spark月', 'Auto', 'Fast', 'Expert', 'Heavy', 'Grok 4.3', '生图']
|
||||
const staticResetLabels = ['日', '5H', '周', '月', '周额度', '月额度', '额度', 'Spark5H', 'Spark周', 'Spark月', 'Auto', 'Fast', 'Expert', 'Heavy', 'Grok 4.3', '生图']
|
||||
if (!item.allowDynamicReset && !staticResetLabels.includes(item.label)) return null
|
||||
if (item.resetAtSeconds == null && item.resetSeconds == null) return null
|
||||
return getCodexResetCountdown(
|
||||
@@ -3567,6 +3574,7 @@ function getQuotaLabelOrder(label: string): number {
|
||||
if (label === 'Prompt') return 12
|
||||
if (label === 'Flex') return 13
|
||||
if (label === '剩余') return 14
|
||||
if (label === '额度') return 14
|
||||
if (label === '最低') return 15
|
||||
if (label === '生图') return 16
|
||||
if (label === '速率') return 17
|
||||
@@ -3758,7 +3766,7 @@ function buildQuotaProgressItemsFromSnapshot(key: PoolKeyDetail): QuotaProgressI
|
||||
.filter((item): item is QuotaProgressItem => item != null)
|
||||
}
|
||||
|
||||
if (providerType === 'kiro') {
|
||||
if (providerType === 'kiro' || providerType === 'xai') {
|
||||
const quotaResetAtSeconds = getQuotaSnapshotResetAtSeconds(quota)
|
||||
const quotaResetSeconds = getQuotaSnapshotResetSeconds(quota)
|
||||
const window = getQuotaSnapshotWindow(quota, 'usage')
|
||||
@@ -3772,12 +3780,13 @@ function buildQuotaProgressItemsFromSnapshot(key: PoolKeyDetail): QuotaProgressI
|
||||
: undefined
|
||||
|
||||
return [{
|
||||
label: '剩余',
|
||||
label: normalizeQuotaLabel(String(window?.label || '').trim() || '剩余'),
|
||||
remainingPercent,
|
||||
detail,
|
||||
resetAtSeconds: normalizeUnixSeconds(window?.reset_at ?? quotaResetAtSeconds ?? null),
|
||||
resetSeconds: normalizeRemainingSeconds(window?.reset_seconds ?? quotaResetSeconds ?? null),
|
||||
updatedAtSeconds: getQuotaSnapshotUpdatedAtSeconds(quota),
|
||||
allowDynamicReset: true,
|
||||
}]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user