mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 09:57:47 +08:00
Merge remote-tracking branch 'origin/main' into fix/gemini-cli-v1internal
# Conflicts: # apps/aether-gateway/src/ai_serving/transport.rs # apps/aether-gateway/src/handlers/admin/provider/oauth/dispatch/batch/parse.rs # apps/aether-gateway/src/handlers/shared/catalog.rs # crates/aether-admin/src/provider/quota.rs # crates/aether-model-fetch/src/strategy.rs # crates/aether-provider-pool/src/lib.rs # crates/aether-provider-pool/src/service.rs # crates/aether-provider-transport/src/provider_types.rs # frontend/src/features/providers/components/ProviderDetailDrawer.vue # frontend/src/utils/__tests__/providerKeyQuota.spec.ts # frontend/src/utils/providerKeyQuota.ts # frontend/src/views/admin/PoolManagement.vue
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
canRefreshOAuthCredential,
|
||||
getProviderMaskedSecretLabel,
|
||||
shouldShowOAuthRefreshControl,
|
||||
} from '@/utils/providerKeyAuth'
|
||||
@@ -17,14 +18,25 @@ describe('providerKeyAuth', () => {
|
||||
expect(shouldShowOAuthRefreshControl(key, 'grok')).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps standard OAuth providers on OAuth token semantics', () => {
|
||||
const key = {
|
||||
it('hides oauth refresh control when backend marks a provider as non-refreshable', () => {
|
||||
const input = {
|
||||
auth_type: 'oauth',
|
||||
oauth_managed: true,
|
||||
can_refresh_oauth: false,
|
||||
}
|
||||
|
||||
expect(getProviderMaskedSecretLabel(key, 'codex')).toBe('[OAuth Token]')
|
||||
expect(shouldShowOAuthRefreshControl(key, 'codex')).toBe(true)
|
||||
expect(canRefreshOAuthCredential(input)).toBe(false)
|
||||
expect(shouldShowOAuthRefreshControl(input)).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps legacy oauth refresh control visible when backend capability is absent', () => {
|
||||
const input = {
|
||||
auth_type: 'oauth',
|
||||
oauth_managed: true,
|
||||
}
|
||||
|
||||
expect(canRefreshOAuthCredential(input)).toBe(true)
|
||||
expect(shouldShowOAuthRefreshControl(input)).toBe(true)
|
||||
expect(getProviderMaskedSecretLabel(input, 'codex')).toBe('[OAuth Token]')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -141,4 +141,122 @@ describe('providerKeyQuota', () => {
|
||||
},
|
||||
}, 'gemini_cli')).toBe('AI Credits 剩余 41.5')
|
||||
})
|
||||
|
||||
it('surfaces Windsurf hard account states', () => {
|
||||
expect(getQuotaDisplayText({
|
||||
status_snapshot: {
|
||||
quota: {
|
||||
provider_type: 'windsurf',
|
||||
code: 'quarantined',
|
||||
label: '账号隔离中',
|
||||
exhausted: false,
|
||||
},
|
||||
},
|
||||
}, 'windsurf')).toBe('账号隔离中')
|
||||
|
||||
expect(getQuotaDisplayText({
|
||||
status_snapshot: {
|
||||
quota: {
|
||||
provider_type: 'windsurf',
|
||||
code: 'cooldown',
|
||||
label: '冷却中',
|
||||
exhausted: false,
|
||||
},
|
||||
},
|
||||
}, 'windsurf')).toBe('冷却中')
|
||||
|
||||
expect(getQuotaDisplayText({
|
||||
status_snapshot: {
|
||||
quota: {
|
||||
provider_type: 'windsurf',
|
||||
code: 'cooldown',
|
||||
exhausted: false,
|
||||
},
|
||||
},
|
||||
}, 'windsurf')).toBe('冷却中')
|
||||
})
|
||||
|
||||
it('includes Windsurf quota windows and model availability in display text', () => {
|
||||
expect(getQuotaDisplayText({
|
||||
status_snapshot: {
|
||||
quota: {
|
||||
provider_type: 'windsurf',
|
||||
code: 'ok',
|
||||
exhausted: false,
|
||||
allowed_models_count: 7,
|
||||
windows: [
|
||||
{
|
||||
code: 'daily',
|
||||
remaining_ratio: 0.75,
|
||||
},
|
||||
{
|
||||
code: 'weekly',
|
||||
remaining_ratio: 0.5,
|
||||
},
|
||||
{
|
||||
code: 'prompt',
|
||||
remaining_value: 12,
|
||||
limit_value: 20,
|
||||
},
|
||||
{
|
||||
code: 'flex',
|
||||
used_value: 2,
|
||||
limit_value: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}, 'windsurf')).toBe('日剩余 75.0% | 周剩余 50.0% | Prompt 剩余 12/20 | Flex 剩余 3/5 | 可用模型 7 个')
|
||||
|
||||
expect(getQuotaDisplayText({
|
||||
status_snapshot: {
|
||||
quota: {
|
||||
provider_type: 'windsurf',
|
||||
code: 'cooldown',
|
||||
label: '冷却中',
|
||||
exhausted: false,
|
||||
rate_limit: {
|
||||
limited: true,
|
||||
has_capacity: true,
|
||||
messages_remaining: -1,
|
||||
max_messages: -1,
|
||||
},
|
||||
allowed_models_count: 118,
|
||||
windows: [
|
||||
{
|
||||
code: 'daily',
|
||||
remaining_ratio: 0.99,
|
||||
},
|
||||
{
|
||||
code: 'weekly',
|
||||
remaining_ratio: 1,
|
||||
},
|
||||
{
|
||||
code: 'prompt',
|
||||
remaining_value: 100,
|
||||
limit_value: 100,
|
||||
},
|
||||
{
|
||||
code: 'rate_limit',
|
||||
reset_seconds: null,
|
||||
is_exhausted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}, 'windsurf')).toBe('日剩余 99.0% | 周剩余 100.0% | Prompt 剩余 100/100 | 可用模型 118 个')
|
||||
})
|
||||
|
||||
it('uses Windsurf model availability when no quota window is present', () => {
|
||||
expect(getQuotaDisplayText({
|
||||
status_snapshot: {
|
||||
quota: {
|
||||
provider_type: 'windsurf',
|
||||
code: 'ok',
|
||||
exhausted: false,
|
||||
allowed_models_count: 3,
|
||||
},
|
||||
},
|
||||
}, 'windsurf')).toBe('可用模型 3 个')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -251,4 +251,20 @@ describe('providerKeyStatus', () => {
|
||||
})
|
||||
expect(getOAuthStatusTitle(input, 0)).toBe('Refresh Token 未添加,无法自动刷新')
|
||||
})
|
||||
|
||||
it('does not treat non-refreshable provider sessions as missing refresh token', () => {
|
||||
const input = {
|
||||
auth_type: 'oauth',
|
||||
oauth_managed: true,
|
||||
can_refresh_oauth: false,
|
||||
}
|
||||
|
||||
expect(getOAuthStatusDisplayWithFallback(input, 0)).toEqual({
|
||||
text: '有效期未知',
|
||||
isExpired: false,
|
||||
isExpiringSoon: false,
|
||||
isInvalid: false,
|
||||
})
|
||||
expect(getOAuthStatusTitle(input, 0)).toBe('Token 有效期未知')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,6 +3,10 @@ export interface ChatPiiRedactionFeatureSettings {
|
||||
inject_model_instruction: boolean
|
||||
}
|
||||
|
||||
export interface NotificationPushServiceFeatureSettings {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export type FeatureSettingsMap = Record<string, unknown>
|
||||
|
||||
const DEFAULT_CHAT_PII_REDACTION_FEATURE_SETTINGS: ChatPiiRedactionFeatureSettings = {
|
||||
@@ -10,6 +14,10 @@ const DEFAULT_CHAT_PII_REDACTION_FEATURE_SETTINGS: ChatPiiRedactionFeatureSettin
|
||||
inject_model_instruction: true,
|
||||
}
|
||||
|
||||
const DEFAULT_NOTIFICATION_PUSH_SERVICE_FEATURE_SETTINGS: NotificationPushServiceFeatureSettings = {
|
||||
enabled: false,
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
@@ -49,3 +57,30 @@ export function mergeChatPiiRedactionFeatureSettings(
|
||||
}
|
||||
return Object.keys(settings).length > 0 ? settings : null
|
||||
}
|
||||
|
||||
export function readNotificationPushServiceFeatureSettings(
|
||||
featureSettings: unknown,
|
||||
): NotificationPushServiceFeatureSettings {
|
||||
const feature = isRecord(featureSettings)
|
||||
? featureSettings.notification_push_service
|
||||
: null
|
||||
if (!isRecord(feature)) {
|
||||
return { ...DEFAULT_NOTIFICATION_PUSH_SERVICE_FEATURE_SETTINGS }
|
||||
}
|
||||
return {
|
||||
enabled: feature.enabled === true,
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeNotificationPushServiceFeatureSettings(
|
||||
featureSettings: unknown,
|
||||
notificationPushService: NotificationPushServiceFeatureSettings,
|
||||
): FeatureSettingsMap | null {
|
||||
const settings: FeatureSettingsMap = isRecord(featureSettings)
|
||||
? { ...featureSettings }
|
||||
: {}
|
||||
settings.notification_push_service = {
|
||||
enabled: notificationPushService.enabled,
|
||||
}
|
||||
return Object.keys(settings).length > 0 ? settings : null
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ export function shouldShowOAuthRefreshControl(
|
||||
providerType?: string | null,
|
||||
): boolean {
|
||||
if (isGrokSessionCredential(input, providerType)) return false
|
||||
return isOAuthManagedCredential(input)
|
||||
return canRefreshOAuthCredential(input)
|
||||
}
|
||||
|
||||
export function canExportOAuthCredential(input: ProviderKeyAuthCarrier): boolean {
|
||||
|
||||
@@ -100,6 +100,25 @@ function objectValue(value: unknown): Record<string, unknown> | null {
|
||||
? value as Record<string, unknown>
|
||||
: null
|
||||
}
|
||||
|
||||
function positiveNumber(value: unknown): number | null {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null
|
||||
}
|
||||
|
||||
function windsurfCooldownHasPositiveReset(quota: QuotaStatusSnapshot): boolean {
|
||||
const rateLimit = quota.rate_limit
|
||||
if (rateLimit && typeof rateLimit === 'object') {
|
||||
const retryAfterMs = positiveNumber(rateLimit.retry_after_ms) ?? positiveNumber(rateLimit.retryAfterMs)
|
||||
if (retryAfterMs != null) return true
|
||||
}
|
||||
|
||||
const rateLimitWindow = getQuotaWindow(quota, 'rate_limit')
|
||||
return (
|
||||
positiveNumber(rateLimitWindow?.reset_seconds) != null
|
||||
|| positiveNumber(rateLimitWindow?.reset_at) != null
|
||||
)
|
||||
}
|
||||
|
||||
function getQuotaWindowsByScope(
|
||||
quota: QuotaStatusSnapshot | null | undefined,
|
||||
scope: string,
|
||||
@@ -290,6 +309,60 @@ function getGrokQuotaText(quota: QuotaStatusSnapshot): string | null {
|
||||
return normalizeText(quota.label)
|
||||
}
|
||||
|
||||
function getWindsurfQuotaText(quota: QuotaStatusSnapshot): string | null {
|
||||
const code = normalizeText(quota.code)?.toLowerCase()
|
||||
if (code === 'banned' || code === 'forbidden' || code === 'quarantined') {
|
||||
return normalizeText(quota.label) || '账号不可用'
|
||||
}
|
||||
if (code === 'cooldown' && windsurfCooldownHasPositiveReset(quota)) {
|
||||
return normalizeText(quota.label) || '冷却中'
|
||||
}
|
||||
if (code === 'rate_limited' || code === 'rate_limit') {
|
||||
return normalizeText(quota.label) || '速率受限'
|
||||
}
|
||||
if (code === 'exhausted') {
|
||||
return normalizeText(quota.label) || '额度已耗尽'
|
||||
}
|
||||
|
||||
const parts: string[] = []
|
||||
const dailyRemaining = getQuotaWindowRemainingPercent(getQuotaWindow(quota, 'daily'))
|
||||
const weeklyRemaining = getQuotaWindowRemainingPercent(getQuotaWindow(quota, 'weekly'))
|
||||
if (dailyRemaining != null) parts.push(`日剩余 ${formatPercent(dailyRemaining)}`)
|
||||
if (weeklyRemaining != null) parts.push(`周剩余 ${formatPercent(weeklyRemaining)}`)
|
||||
|
||||
for (const [label, code] of [
|
||||
['Prompt', 'prompt'],
|
||||
['Flex', 'flex'],
|
||||
] as const) {
|
||||
const window = getQuotaWindow(quota, code)
|
||||
if (!window) continue
|
||||
if (typeof window.remaining_value === 'number' && typeof window.limit_value === 'number' && window.limit_value > 0) {
|
||||
parts.push(`${label} 剩余 ${formatQuotaValue(window.remaining_value)}/${formatQuotaValue(window.limit_value)}`)
|
||||
continue
|
||||
}
|
||||
if (typeof window.used_value === 'number' && typeof window.limit_value === 'number' && window.limit_value > 0) {
|
||||
parts.push(`${label} 剩余 ${formatQuotaValue(Math.max(window.limit_value - window.used_value, 0))}/${formatQuotaValue(window.limit_value)}`)
|
||||
continue
|
||||
}
|
||||
const remainingPercent = getQuotaWindowRemainingPercent(window)
|
||||
if (remainingPercent != null) {
|
||||
parts.push(`${label} 剩余 ${formatPercent(remainingPercent)}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof quota.allowed_models_count === 'number') {
|
||||
parts.push(`可用模型 ${quota.allowed_models_count} 个`)
|
||||
}
|
||||
|
||||
if (parts.length > 0) return parts.join(' | ')
|
||||
|
||||
if (code === 'cooldown') {
|
||||
return normalizeText(quota.label) || '冷却中'
|
||||
}
|
||||
|
||||
return normalizeText(quota.label)
|
||||
}
|
||||
|
||||
function getAntigravityQuotaText(quota: QuotaStatusSnapshot): string | null {
|
||||
const code = normalizeText(quota.code)?.toLowerCase()
|
||||
if (code === 'forbidden') {
|
||||
@@ -390,6 +463,8 @@ export function getQuotaSnapshotFallbackText(
|
||||
return getKiroQuotaText(quota)
|
||||
case 'grok':
|
||||
return getGrokQuotaText(quota)
|
||||
case 'windsurf':
|
||||
return getWindsurfQuotaText(quota)
|
||||
case 'antigravity':
|
||||
return getAntigravityQuotaText(quota)
|
||||
case 'gemini_cli':
|
||||
|
||||
@@ -202,7 +202,7 @@ function mergeOAuthStatusDisplay(
|
||||
}
|
||||
|
||||
function isOAuthCredentialWithoutRefreshToken(input: ProviderKeyStatusCarrier): boolean {
|
||||
return isOAuthManagedCredential(input) && !canRefreshOAuthCredential(input)
|
||||
return isOAuthManagedCredential(input) && input.oauth_temporary === true
|
||||
}
|
||||
|
||||
function getMissingRefreshTokenStatus(): OAuthStatusInfo {
|
||||
|
||||
Reference in New Issue
Block a user