mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 02:47:45 +08:00
fix(frontend): 优化移动端弹窗并完善提供商配额刷新
This commit is contained in:
@@ -993,10 +993,6 @@ import type {
|
||||
} from '@/api/endpoints/types'
|
||||
import { formatApiFormatShort } from '@/api/endpoints/types/api-format'
|
||||
import { isOAuthAccountProviderType, isKeyManagedProviderType } from '../utils/providerTypeUtils'
|
||||
import {
|
||||
isProviderQuotaAutoRefreshCoolingDown,
|
||||
markProviderQuotaAutoRefreshAttempt,
|
||||
} from '../utils/quotaAutoRefreshCooldown'
|
||||
import { getOAuthOrgBadge } from '@/utils/oauthIdentity'
|
||||
import { getOAuthRefreshFeedback } from '@/utils/oauthRefreshFeedback'
|
||||
import { formatCompactNumber } from '@/utils/format'
|
||||
@@ -1022,6 +1018,7 @@ import {
|
||||
formatCodexResetCreditDays,
|
||||
getCodexResetCreditAvailableCount as getCodexResetCreditAvailableCountFromSnapshot,
|
||||
getVisibleCodexResetCreditItems as getVisibleCodexResetCreditItemsFromSnapshot,
|
||||
mergeCodexQuotaDisplays,
|
||||
} from './codex-reset-credit-display'
|
||||
|
||||
// 扩展端点类型,包含密钥列表
|
||||
@@ -1600,7 +1597,7 @@ async function handleRefreshOAuth(key: EndpointAPIKey) {
|
||||
}
|
||||
// Antigravity:token 刷新后可能完成了账号激活,触发配额获取
|
||||
// (不 emit('refresh'),避免触发全局 provider 余额刷新)
|
||||
void autoRefreshQuotaInBackground({ ignoreCooldown: true })
|
||||
void autoRefreshQuotaInBackground()
|
||||
} catch (err: unknown) {
|
||||
showError(localizedApiError(err, 'Token 刷新失败'), legacyT('错误'))
|
||||
} finally {
|
||||
@@ -1951,20 +1948,6 @@ function getCodexQuotaDisplayFromSnapshot(quota: QuotaStatusSnapshot | null | un
|
||||
return Object.keys(display).length > 0 ? display : null
|
||||
}
|
||||
|
||||
function getCodexQuotaDisplayUpdatedAt(display: CodexUpstreamMetadata | null | undefined): number | null {
|
||||
const updatedAt = Number(display?.updated_at)
|
||||
return Number.isFinite(updatedAt) ? updatedAt : null
|
||||
}
|
||||
|
||||
function codexDisplayHasUsage(display: CodexUpstreamMetadata | null | undefined): boolean {
|
||||
return !!display && (
|
||||
display.primary_used_percent !== undefined
|
||||
|| display.secondary_used_percent !== undefined
|
||||
|| display.spark_primary_used_percent !== undefined
|
||||
|| display.spark_secondary_used_percent !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
function codexDisplayHasResetCredits(display: CodexUpstreamMetadata | null | undefined): boolean {
|
||||
const count = display?.reset_credits?.available_count
|
||||
return typeof count === 'number' && Number.isFinite(count)
|
||||
@@ -1973,20 +1956,7 @@ function codexDisplayHasResetCredits(display: CodexUpstreamMetadata | null | und
|
||||
function getCodexQuotaDisplay(key: EndpointAPIKey): CodexUpstreamMetadata | null {
|
||||
const snapshotDisplay = getCodexQuotaDisplayFromSnapshot(getQuotaSnapshotForProvider(key, 'codex'))
|
||||
const metadataDisplay = getCodexQuotaDisplayFromMetadata(key.upstream_metadata?.codex)
|
||||
|
||||
if (!snapshotDisplay) return metadataDisplay
|
||||
if (!metadataDisplay) return snapshotDisplay
|
||||
if (!codexDisplayHasUsage(snapshotDisplay) && codexDisplayHasUsage(metadataDisplay)) {
|
||||
return metadataDisplay
|
||||
}
|
||||
|
||||
const snapshotUpdatedAt = getCodexQuotaDisplayUpdatedAt(snapshotDisplay)
|
||||
const metadataUpdatedAt = getCodexQuotaDisplayUpdatedAt(metadataDisplay)
|
||||
if (metadataUpdatedAt !== null && (snapshotUpdatedAt === null || metadataUpdatedAt > snapshotUpdatedAt)) {
|
||||
return metadataDisplay
|
||||
}
|
||||
|
||||
return snapshotDisplay
|
||||
return mergeCodexQuotaDisplays(snapshotDisplay, metadataDisplay)
|
||||
}
|
||||
|
||||
function hasCodexQuotaDisplayData(key: EndpointAPIKey): boolean {
|
||||
@@ -2714,7 +2684,7 @@ function applyQuotaResults(
|
||||
}
|
||||
|
||||
// 通用的自动刷新配额函数(支持 Codex、Gemini CLI、Antigravity、Kiro、Windsurf 和 ChatGPT Web)
|
||||
async function autoRefreshQuotaInBackground(options: { ignoreCooldown?: boolean } = {}) {
|
||||
async function autoRefreshQuotaInBackground() {
|
||||
const providerId = props.providerId
|
||||
if (!providerId) return
|
||||
if (refreshingQuota.value) return
|
||||
@@ -2740,7 +2710,6 @@ async function autoRefreshQuotaInBackground(options: { ignoreCooldown?: boolean
|
||||
shouldRefresh = shouldAutoRefreshChatGPTWebQuota()
|
||||
}
|
||||
if (!shouldRefresh) return
|
||||
if (!options.ignoreCooldown && isProviderQuotaAutoRefreshCoolingDown(providerId)) return
|
||||
|
||||
let hadCachedQuota = false
|
||||
if (providerType === 'codex') {
|
||||
@@ -2760,7 +2729,6 @@ async function autoRefreshQuotaInBackground(options: { ignoreCooldown?: boolean
|
||||
}
|
||||
|
||||
refreshingQuota.value = true
|
||||
markProviderQuotaAutoRefreshAttempt(providerId)
|
||||
try {
|
||||
const result = await refreshProviderQuota(providerId)
|
||||
const applied = applyQuotaResults(result.results)
|
||||
@@ -2804,7 +2772,7 @@ async function handleKeyChanged() {
|
||||
await Promise.all([loadEndpoints(), loadMappingPreview()])
|
||||
emit('refresh')
|
||||
// 添加/修改 key 后自动获取已支持 provider 的配额(新 key 的 upstream_metadata 为空)
|
||||
void autoRefreshQuotaInBackground({ ignoreCooldown: true })
|
||||
void autoRefreshQuotaInBackground()
|
||||
}
|
||||
|
||||
// 切换密钥启用状态
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
@@ -67,8 +67,10 @@ const nodeOptions = computed(() => {
|
||||
|
||||
/** 供父组件调用:启用代理时懒加载节点列表 */
|
||||
function ensureLoaded() {
|
||||
proxyNodesStore.ensureLoaded()
|
||||
return proxyNodesStore.ensureLoaded()
|
||||
}
|
||||
|
||||
onMounted(ensureLoaded)
|
||||
|
||||
defineExpose({ ensureLoaded })
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createApp, defineComponent, h } from 'vue'
|
||||
|
||||
import ProxyNodeSelect from '@/features/providers/components/ProxyNodeSelect.vue'
|
||||
import { createI18n } from '@/i18n'
|
||||
|
||||
const proxyNodesStore = vi.hoisted(() => ({
|
||||
loading: false,
|
||||
nodes: [],
|
||||
onlineNodes: [],
|
||||
ensureLoaded: vi.fn(() => Promise.resolve()),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/proxy-nodes', () => ({
|
||||
useProxyNodesStore: () => proxyNodesStore,
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui', async () => {
|
||||
const { defineComponent, h } = await import('vue')
|
||||
const passthrough = (name: string) => defineComponent({
|
||||
name,
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', slots.default?.())
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
Select: passthrough('SelectStub'),
|
||||
SelectTrigger: passthrough('SelectTriggerStub'),
|
||||
SelectValue: passthrough('SelectValueStub'),
|
||||
SelectContent: passthrough('SelectContentStub'),
|
||||
SelectItem: passthrough('SelectItemStub'),
|
||||
}
|
||||
})
|
||||
|
||||
describe('ProxyNodeSelect', () => {
|
||||
it('loads proxy nodes when mounted', () => {
|
||||
const root = document.createElement('div')
|
||||
const app = createApp(defineComponent({
|
||||
setup() {
|
||||
return () => h(ProxyNodeSelect, { modelValue: '' })
|
||||
},
|
||||
}))
|
||||
|
||||
app.use(createI18n())
|
||||
app.mount(root)
|
||||
|
||||
expect(proxyNodesStore.ensureLoaded).toHaveBeenCalledTimes(1)
|
||||
|
||||
app.unmount()
|
||||
})
|
||||
})
|
||||
+30
@@ -5,10 +5,40 @@ import {
|
||||
formatCodexResetCreditDays,
|
||||
getCodexResetCreditAvailableCount,
|
||||
getVisibleCodexResetCreditItems,
|
||||
mergeCodexQuotaDisplays,
|
||||
} from '@/features/providers/components/codex-reset-credit-display'
|
||||
import type { QuotaResetCreditsSnapshot } from '@/api/endpoints/types'
|
||||
|
||||
describe('codex reset credit display helpers', () => {
|
||||
it('keeps reset credits and usage windows when snapshot sources are partially populated', () => {
|
||||
const merged = mergeCodexQuotaDisplays(
|
||||
{
|
||||
updated_at: 1_700_000_100,
|
||||
primary_used_percent: 25,
|
||||
reset_credits: {
|
||||
available_count: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
updated_at: 1_700_000_000,
|
||||
secondary_used_percent: 40,
|
||||
reset_credits: {
|
||||
credits: [{ id: 'credit-1', expires_at: 1_700_086_400 }],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
expect(merged).toMatchObject({
|
||||
updated_at: 1_700_000_100,
|
||||
primary_used_percent: 25,
|
||||
secondary_used_percent: 40,
|
||||
reset_credits: {
|
||||
available_count: 2,
|
||||
credits: [{ id: 'credit-1', expires_at: 1_700_086_400 }],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps zero available credits displayable but non-positive detail items hidden', () => {
|
||||
const snapshot: QuotaResetCreditsSnapshot = {
|
||||
available_count: 0,
|
||||
|
||||
@@ -1,8 +1,41 @@
|
||||
import type {
|
||||
CodexUpstreamMetadata,
|
||||
QuotaResetCreditSnapshot,
|
||||
QuotaResetCreditsSnapshot,
|
||||
} from '@/api/endpoints/types'
|
||||
|
||||
function codexQuotaUpdatedAt(display: CodexUpstreamMetadata | null | undefined): number | null {
|
||||
const updatedAt = Number(display?.updated_at)
|
||||
return Number.isFinite(updatedAt) ? updatedAt : null
|
||||
}
|
||||
|
||||
export function mergeCodexQuotaDisplays(
|
||||
snapshotDisplay: CodexUpstreamMetadata | null | undefined,
|
||||
metadataDisplay: CodexUpstreamMetadata | null | undefined,
|
||||
): CodexUpstreamMetadata | null {
|
||||
if (!snapshotDisplay) return metadataDisplay ?? null
|
||||
if (!metadataDisplay) return snapshotDisplay
|
||||
|
||||
const snapshotUpdatedAt = codexQuotaUpdatedAt(snapshotDisplay)
|
||||
const metadataUpdatedAt = codexQuotaUpdatedAt(metadataDisplay)
|
||||
const metadataIsNewer = metadataUpdatedAt !== null
|
||||
&& (snapshotUpdatedAt === null || metadataUpdatedAt > snapshotUpdatedAt)
|
||||
const preferred = metadataIsNewer ? metadataDisplay : snapshotDisplay
|
||||
const fallback = metadataIsNewer ? snapshotDisplay : metadataDisplay
|
||||
const resetCredits = preferred.reset_credits || fallback.reset_credits
|
||||
? {
|
||||
...fallback.reset_credits,
|
||||
...preferred.reset_credits,
|
||||
}
|
||||
: undefined
|
||||
|
||||
return {
|
||||
...fallback,
|
||||
...preferred,
|
||||
...(resetCredits ? { reset_credits: resetCredits } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
export interface CodexResetCreditDisplayItem {
|
||||
id?: string | null
|
||||
displayKey: string
|
||||
|
||||
Reference in New Issue
Block a user