mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
Merge pull request #167 from AAEE86/dev
feat: Codex/Kiro 配额自动刷新增加 5 分钟过期检查
This commit is contained in:
@@ -876,8 +876,11 @@
|
||||
:key="`models-${provider.id}`"
|
||||
:provider="provider"
|
||||
:endpoints="endpoints"
|
||||
:models="providerModels"
|
||||
:mapping-preview="providerMappingPreview"
|
||||
@edit-model="handleEditModel"
|
||||
@batch-assign="handleBatchAssign"
|
||||
@refresh="loadEndpoints"
|
||||
/>
|
||||
|
||||
<!-- 模型映射 -->
|
||||
@@ -887,6 +890,9 @@
|
||||
:key="`mapping-${provider.id}`"
|
||||
:provider="provider"
|
||||
:provider-keys="providerKeys"
|
||||
:models="providerModels"
|
||||
:mapping-preview="providerMappingPreview"
|
||||
:endpoints="endpoints"
|
||||
@refresh="handleModelMappingChanged"
|
||||
/>
|
||||
</div>
|
||||
@@ -1027,7 +1033,14 @@ import { useToast } from '@/composables/useToast'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { useCountdownTimer, formatCountdown, getOAuthExpiresCountdown } from '@/composables/useCountdownTimer'
|
||||
import { getProvider, getProviderEndpoints, updateProvider } from '@/api/endpoints'
|
||||
import {
|
||||
getProvider,
|
||||
getProviderEndpoints,
|
||||
updateProvider,
|
||||
getProviderModels,
|
||||
getProviderMappingPreview,
|
||||
type ProviderMappingPreviewResponse
|
||||
} from '@/api/endpoints'
|
||||
import { adminApi } from '@/api/admin'
|
||||
import {
|
||||
KeyFormDialog,
|
||||
@@ -1092,6 +1105,8 @@ const loading = ref(false)
|
||||
const provider = ref<any>(null)
|
||||
const endpoints = ref<ProviderEndpointWithKeys[]>([])
|
||||
const providerKeys = ref<EndpointAPIKey[]>([]) // Provider 级别的 keys
|
||||
const providerModels = ref<Model[]>([]) // Provider 级别的 models
|
||||
const providerMappingPreview = ref<ProviderMappingPreviewResponse | null>(null) // 映射预览
|
||||
|
||||
// 系统级格式转换配置
|
||||
const systemFormatConversionEnabled = ref(false)
|
||||
@@ -1632,21 +1647,29 @@ function formatKiroSubscription(title: string | undefined): string {
|
||||
|
||||
function shouldAutoRefreshCodexQuota(): boolean {
|
||||
if (provider.value?.provider_type !== 'codex') 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
|
||||
|
||||
const meta: UpstreamMetadata | null | undefined = key.upstream_metadata
|
||||
// 只要有一个活跃 key 没有配额数据,就刷新一次
|
||||
if (!hasCodexQuotaData(meta)) {
|
||||
return true
|
||||
}
|
||||
// 配额数据超过 5 分钟未更新,也触发刷新
|
||||
const updatedAt = meta?.codex?.updated_at
|
||||
if (typeof updatedAt !== 'number' || (now - updatedAt) > AUTO_QUOTA_REFRESH_STALE_SECONDS) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查 OAuth Token 是否即将过期(Antigravity / Kiro )
|
||||
// 检查 OAuth Token 是否即将过期(Codex / Antigravity / Kiro)
|
||||
function isTokenExpiringSoon(key: EndpointAPIKey, now: number): boolean {
|
||||
return key.oauth_invalid_at == null
|
||||
&& typeof key.oauth_expires_at === 'number'
|
||||
@@ -1687,8 +1710,14 @@ function shouldAutoRefreshKiroQuota(): boolean {
|
||||
|
||||
if (isTokenExpiringSoon(key, now)) return true
|
||||
|
||||
const meta = key.upstream_metadata
|
||||
// 只要有一个活跃 key 没有配额数据,就刷新一次
|
||||
if (!hasKiroQuotaData(key.upstream_metadata)) {
|
||||
if (!hasKiroQuotaData(meta)) {
|
||||
return true
|
||||
}
|
||||
// 配额数据超过 5 分钟未更新,也触发刷新
|
||||
const updatedAt = meta?.kiro?.updated_at
|
||||
if (typeof updatedAt !== 'number' || (now - updatedAt) > AUTO_QUOTA_REFRESH_STALE_SECONDS) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -2525,13 +2554,17 @@ async function loadEndpoints() {
|
||||
if (!props.providerId) return
|
||||
|
||||
try {
|
||||
// 并行加载端点列表和 Provider 级别的 keys
|
||||
const [endpointsList, providerKeysResult] = await Promise.all([
|
||||
// 并行加载端点列表、Provider 级别的 keys、models 和映射预览
|
||||
const [endpointsList, providerKeysResult, modelsResult, mappingPreviewResult] = await Promise.all([
|
||||
getProviderEndpoints(props.providerId),
|
||||
getProviderKeys(props.providerId).catch(() => []),
|
||||
getProviderModels(props.providerId).catch(() => []),
|
||||
getProviderMappingPreview(props.providerId).catch(() => null),
|
||||
])
|
||||
|
||||
providerKeys.value = providerKeysResult
|
||||
providerModels.value = modelsResult
|
||||
providerMappingPreview.value = mappingPreviewResult
|
||||
// 按 API 格式排序
|
||||
endpoints.value = endpointsList.sort((a, b) => {
|
||||
const aIdx = API_FORMAT_ORDER.indexOf(a.api_format)
|
||||
|
||||
@@ -343,7 +343,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useSmartPagination } from '@/composables/useSmartPagination'
|
||||
import { Tag, Plus, Edit, Trash2, ChevronRight, Loader2, Play } from 'lucide-vue-next'
|
||||
import {
|
||||
@@ -354,15 +354,12 @@ import AlertDialog from '@/components/common/AlertDialog.vue'
|
||||
import ModelMappingDialog, { type AliasGroup } from '../ModelMappingDialog.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import {
|
||||
getProviderModels,
|
||||
getProviderMappingPreview,
|
||||
testModel,
|
||||
type Model,
|
||||
type ProviderModelAlias,
|
||||
type ProviderMappingPreviewResponse
|
||||
} from '@/api/endpoints'
|
||||
import { getProviderEndpoints } from '@/api/endpoints/endpoints'
|
||||
import { getProviderKeys, type EndpointAPIKey } from '@/api/endpoints/keys'
|
||||
import { type EndpointAPIKey } from '@/api/endpoints/keys'
|
||||
import type { ProviderEndpoint } from '@/api/endpoints/types'
|
||||
import { updateModel } from '@/api/endpoints/models'
|
||||
import { parseTestModelError } from '@/utils/errorParser'
|
||||
@@ -394,6 +391,9 @@ interface CombinedMapping {
|
||||
const props = defineProps<{
|
||||
provider: any
|
||||
providerKeys?: EndpointAPIKey[]
|
||||
models?: Model[]
|
||||
mappingPreview?: ProviderMappingPreviewResponse | null
|
||||
endpoints?: ProviderEndpoint[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -404,8 +404,6 @@ const { error: showError, success: showSuccess } = useToast()
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const models = ref<Model[]>([])
|
||||
const aliasMappingPreview = ref<ProviderMappingPreviewResponse | null>(null)
|
||||
const dialogOpen = ref(false)
|
||||
const deleteConfirmOpen = ref(false)
|
||||
const editingGroup = ref<AliasGroup | null>(null)
|
||||
@@ -413,22 +411,21 @@ const deletingGroup = ref<AliasGroup | null>(null)
|
||||
const testingMapping = ref<string | null>(null)
|
||||
const preselectedModelId = ref<string | null>(null)
|
||||
|
||||
// 端点数据(用于测试格式选择)
|
||||
const providerEndpoints = ref<ProviderEndpoint[]>([])
|
||||
|
||||
// Key 数据(用于判断支持的格式)
|
||||
const providerKeysState = ref<EndpointAPIKey[]>([])
|
||||
|
||||
// 测试下拉菜单状态
|
||||
const formatMenuOpen = ref<Record<string, boolean>>({})
|
||||
|
||||
// 使用 props 传入的数据
|
||||
const models = computed(() => props.models ?? [])
|
||||
const aliasMappingPreview = computed(() => props.mappingPreview ?? null)
|
||||
const providerEndpoints = computed(() => props.endpoints ?? [])
|
||||
const providerKeysState = computed(() => props.providerKeys ?? [])
|
||||
|
||||
// 展开状态
|
||||
const expandedItems = ref<Set<string>>(new Set())
|
||||
|
||||
// 是否有 key 配置了自动获取上游模型
|
||||
const hasAutoFetchKey = computed(() => {
|
||||
const keys = props.providerKeys || providerKeysState.value
|
||||
return keys.some(k => k.auto_fetch_models)
|
||||
return providerKeysState.value.some(k => k.auto_fetch_models)
|
||||
})
|
||||
|
||||
// 生成作用域唯一键
|
||||
@@ -562,25 +559,9 @@ const {
|
||||
paginatedItems: paginatedMappings,
|
||||
} = useSmartPagination(combinedMappings, mappingsListRef)
|
||||
|
||||
// 加载数据
|
||||
async function loadData() {
|
||||
try {
|
||||
loading.value = true
|
||||
const [modelsData, previewData, endpointsData, keysData] = await Promise.all([
|
||||
getProviderModels(props.provider.id),
|
||||
getProviderMappingPreview(props.provider.id).catch(() => null),
|
||||
getProviderEndpoints(props.provider.id).catch(() => []),
|
||||
getProviderKeys(props.provider.id).catch(() => [])
|
||||
])
|
||||
models.value = modelsData
|
||||
aliasMappingPreview.value = previewData
|
||||
providerEndpoints.value = endpointsData
|
||||
providerKeysState.value = keysData
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '加载失败', '错误')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
// 刷新数据(通知父组件刷新)
|
||||
function refresh() {
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
// 删除确认描述
|
||||
@@ -653,7 +634,6 @@ async function confirmDelete() {
|
||||
showSuccess('映射已删除')
|
||||
deleteConfirmOpen.value = false
|
||||
deletingGroup.value = null
|
||||
await loadData()
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '删除失败', '错误')
|
||||
@@ -662,7 +642,6 @@ async function confirmDelete() {
|
||||
|
||||
// 对话框保存后回调
|
||||
async function onDialogSaved() {
|
||||
await loadData()
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
@@ -770,16 +749,9 @@ async function testRegexMapping(item: CombinedMapping, keyItem: MatchedKeyInfo,
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 provider 变化
|
||||
watch(() => props.provider?.id, (newId) => {
|
||||
if (newId) {
|
||||
loadData()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 暴露给父组件
|
||||
defineExpose({
|
||||
dialogOpen: computed(() => dialogOpen.value || deleteConfirmOpen.value),
|
||||
reload: loadData
|
||||
reload: refresh
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -246,7 +246,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useSmartPagination } from '@/composables/useSmartPagination'
|
||||
import { Box, Edit, Layers, Power, Copy, Loader2, Play } from 'lucide-vue-next'
|
||||
import Card from '@/components/ui/card.vue'
|
||||
@@ -261,8 +261,6 @@ import { useToast } from '@/composables/useToast'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { sortResolutionEntries } from '@/utils/form'
|
||||
import {
|
||||
getProviderModels,
|
||||
getProviderMappingPreview,
|
||||
testModel,
|
||||
type Model,
|
||||
type ProviderMappingPreviewResponse
|
||||
@@ -280,11 +278,14 @@ interface Endpoint {
|
||||
const props = defineProps<{
|
||||
provider: any
|
||||
endpoints?: Endpoint[]
|
||||
models?: Model[]
|
||||
mappingPreview?: ProviderMappingPreviewResponse | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'editModel': [model: Model]
|
||||
'batchAssign': []
|
||||
'refresh': []
|
||||
}>()
|
||||
|
||||
const { error: showError, success: showSuccess } = useToast()
|
||||
@@ -292,12 +293,16 @@ const { copyToClipboard } = useClipboard()
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const models = ref<Model[]>([])
|
||||
const mappingPreview = ref<ProviderMappingPreviewResponse | null>(null)
|
||||
const localModels = ref<Model[]>([])
|
||||
const localMappingPreview = ref<ProviderMappingPreviewResponse | null>(null)
|
||||
const togglingModelId = ref<string | null>(null)
|
||||
const testingModelId = ref<string | null>(null)
|
||||
const formatMenuOpen = ref<Record<string, boolean>>({})
|
||||
|
||||
// 使用 props 传入的数据,或使用本地数据
|
||||
const models = computed(() => props.models ?? localModels.value)
|
||||
const mappingPreview = computed(() => props.mappingPreview ?? localMappingPreview.value)
|
||||
|
||||
// 获取可用的 API 格式(有活跃端点且有活跃 Key)
|
||||
const availableApiFormats = computed(() => {
|
||||
if (!props.endpoints) return []
|
||||
@@ -329,24 +334,9 @@ async function copyModelId(modelId: string) {
|
||||
await copyToClipboard(modelId)
|
||||
}
|
||||
|
||||
// 加载模型和映射预览
|
||||
async function loadModels() {
|
||||
try {
|
||||
loading.value = true
|
||||
const [modelsData, previewData] = await Promise.all([
|
||||
getProviderModels(props.provider.id),
|
||||
getProviderMappingPreview(props.provider.id).catch((err) => {
|
||||
console.warn('Failed to load mapping preview:', err)
|
||||
return null
|
||||
})
|
||||
])
|
||||
models.value = modelsData
|
||||
mappingPreview.value = previewData
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '加载失败', '错误')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
// 刷新数据(通知父组件刷新)
|
||||
function refresh() {
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
// 格式化价格显示
|
||||
@@ -547,12 +537,8 @@ async function testModelConnection(model: Model, apiFormat?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadModels()
|
||||
})
|
||||
|
||||
// 暴露给父组件
|
||||
defineExpose({
|
||||
reload: loadModels
|
||||
reload: refresh
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user