Merge pull request #167 from AAEE86/dev

feat: Codex/Kiro 配额自动刷新增加 5 分钟过期检查
This commit is contained in:
fawney19
2026-02-11 01:10:19 +08:00
committed by GitHub
8 changed files with 225 additions and 123 deletions

View File

@@ -126,11 +126,48 @@ const canUseHourly = computed(() => {
return false return false
}) })
// 记录上次 emit 的值,避免重复触发
let lastEmittedValue: string | null = null
function buildEmitValue(): DateRangeParams {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
const tz_offset_minutes = -new Date().getTimezoneOffset()
if (selectedPreset.value === 'custom') {
const start = startDate.value <= endDate.value ? startDate.value : endDate.value
const end = endDate.value >= startDate.value ? endDate.value : startDate.value
return {
start_date: start,
end_date: end,
granularity: selectedGranularity.value,
timezone,
tz_offset_minutes
}
}
return {
preset: selectedPreset.value,
granularity: selectedGranularity.value,
timezone,
tz_offset_minutes
}
}
function getValueKey(value: DateRangeParams): string {
// 只比较核心字段,忽略 timezone 和 tz_offset_minutes这些每次都会重新计算
if (value.preset) {
return `preset:${value.preset}:${value.granularity}`
}
return `custom:${value.start_date}:${value.end_date}:${value.granularity}`
}
watch(() => props.modelValue, (value) => { watch(() => props.modelValue, (value) => {
if (value.preset) selectedPreset.value = value.preset if (value.preset) selectedPreset.value = value.preset
if (value.start_date !== undefined) startDate.value = value.start_date || '' if (value.start_date !== undefined) startDate.value = value.start_date || ''
if (value.end_date !== undefined) endDate.value = value.end_date || '' if (value.end_date !== undefined) endDate.value = value.end_date || ''
if (value.granularity) selectedGranularity.value = value.granularity if (value.granularity) selectedGranularity.value = value.granularity
// 同步更新 lastEmittedValue避免外部设置值后触发重复 emit
lastEmittedValue = getValueKey(value)
}, { deep: true }) }, { deep: true })
watch([selectedPreset, startDate, endDate, selectedGranularity], () => { watch([selectedPreset, startDate, endDate, selectedGranularity], () => {
@@ -140,28 +177,17 @@ watch([selectedPreset, startDate, endDate, selectedGranularity], () => {
} }
} }
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
const tz_offset_minutes = -new Date().getTimezoneOffset()
if (selectedPreset.value === 'custom') { if (selectedPreset.value === 'custom') {
if (!startDate.value || !endDate.value) return if (!startDate.value || !endDate.value) return
const start = startDate.value <= endDate.value ? startDate.value : endDate.value
const end = endDate.value >= startDate.value ? endDate.value : startDate.value
emit('update:modelValue', {
start_date: start,
end_date: end,
granularity: selectedGranularity.value,
timezone,
tz_offset_minutes
})
return
} }
emit('update:modelValue', { const newValue = buildEmitValue()
preset: selectedPreset.value, const newKey = getValueKey(newValue)
granularity: selectedGranularity.value,
timezone, // 只有当值真正变化时才 emit避免初始化时的重复触发
tz_offset_minutes if (newKey !== lastEmittedValue) {
}) lastEmittedValue = newKey
emit('update:modelValue', newValue)
}
}, { immediate: true }) }, { immediate: true })
</script> </script>

View File

@@ -408,6 +408,10 @@
v-if="model" v-if="model"
ref="routingTabRef" ref="routingTabRef"
:global-model-id="model.id" :global-model-id="model.id"
:routing-data="routingData"
:loading="routingLoading"
:error="routingError"
@refresh="loadRoutingData"
@add-provider="$emit('addProvider')" @add-provider="$emit('addProvider')"
@edit-provider="handleEditProviderFromRouting" @edit-provider="handleEditProviderFromRouting"
@toggle-provider-status="handleToggleProviderFromRouting" @toggle-provider-status="handleToggleProviderFromRouting"
@@ -423,7 +427,10 @@
:global-model-id="model.id" :global-model-id="model.id"
:model-name="model.name" :model-name="model.name"
:mappings="model.config?.model_mappings || []" :mappings="model.config?.model_mappings || []"
:routing-data="routingData"
:loading-preview="routingLoading"
@update="handleMappingsUpdate" @update="handleMappingsUpdate"
@refresh="loadRoutingData"
@link-provider="(providerId) => $emit('linkProvider', providerId)" @link-provider="(providerId) => $emit('linkProvider', providerId)"
@link-providers="(providerIds) => $emit('linkProviders', providerIds)" @link-providers="(providerIds) => $emit('linkProviders', providerIds)"
/> />
@@ -462,10 +469,11 @@ import TableCell from '@/components/ui/table-cell.vue'
import RoutingTab from './RoutingTab.vue' import RoutingTab from './RoutingTab.vue'
import ModelMappingsTab from './ModelMappingsTab.vue' import ModelMappingsTab from './ModelMappingsTab.vue'
import { sortResolutionEntries } from '@/utils/form' import { sortResolutionEntries } from '@/utils/form'
import { getGlobalModelRoutingPreview } from '@/api/global-models'
// 使用外部类型定义 // 使用外部类型定义
import type { GlobalModelResponse } from '@/api/global-models' import type { GlobalModelResponse } from '@/api/global-models'
import type { TieredPricingConfig, PricingTier } from '@/api/endpoints/types' import type { TieredPricingConfig, PricingTier, ModelRoutingPreviewResponse } from '@/api/endpoints/types'
import type { CapabilityDefinition } from '@/api/endpoints' import type { CapabilityDefinition } from '@/api/endpoints'
import type { RoutingProviderInfo } from '@/api/global-models' import type { RoutingProviderInfo } from '@/api/global-models'
@@ -498,6 +506,27 @@ const routingTabRef = ref<InstanceType<typeof RoutingTab> | null>(null)
// ModelMappingsTab 引用 // ModelMappingsTab 引用
const modelMappingsTabRef = ref<InstanceType<typeof ModelMappingsTab> | null>(null) const modelMappingsTabRef = ref<InstanceType<typeof ModelMappingsTab> | null>(null)
// 统一管理 routing 数据,避免子组件重复请求
const routingData = ref<ModelRoutingPreviewResponse | null>(null)
const routingLoading = ref(false)
const routingError = ref<string | null>(null)
// 加载 routing 数据(统一入口)
async function loadRoutingData() {
if (!props.model?.id) return
routingLoading.value = true
routingError.value = null
try {
routingData.value = await getGlobalModelRoutingPreview(props.model.id)
} catch (err: any) {
routingError.value = err.response?.data?.detail || '加载失败'
} finally {
routingLoading.value = false
}
}
// 将 RoutingProviderInfo 转换为父组件期望的格式 // 将 RoutingProviderInfo 转换为父组件期望的格式
function convertRoutingProviderToLegacyFormat(provider: RoutingProviderInfo) { function convertRoutingProviderToLegacyFormat(provider: RoutingProviderInfo) {
return { return {
@@ -525,8 +554,7 @@ function handleDeleteProviderFromRouting(provider: RoutingProviderInfo) {
// 刷新路由数据 // 刷新路由数据
function refreshRoutingData() { function refreshRoutingData() {
routingTabRef.value?.loadRoutingData?.() loadRoutingData()
modelMappingsTabRef.value?.refresh?.()
} }
// 处理模型映射更新 // 处理模型映射更新
@@ -632,11 +660,17 @@ function getFirst1hCachePrice(tieredPricing: TieredPricingConfig | undefined | n
return get1hCachePrice(tieredPricing.tiers[0]) return get1hCachePrice(tieredPricing.tiers[0])
} }
// 监听 open 变化,重置 tab // 监听 open 变化,重置 tab 并加载数据
watch(() => props.open, (newOpen) => { watch(() => props.open, (newOpen) => {
if (newOpen) { if (newOpen) {
// 直接设置为 basic不需要先重置为空 // 直接设置为 basic不需要先重置为空
detailTab.value = 'basic' detailTab.value = 'basic'
// 加载 routing 数据
loadRoutingData()
} else {
// 关闭时清空数据
routingData.value = null
routingError.value = null
} }
}) })

View File

@@ -220,7 +220,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, onMounted, onUnmounted, computed } from 'vue' import { ref, watch, onUnmounted, computed } from 'vue'
import { Card, Button, Input, Badge } from '@/components/ui' import { Card, Button, Input, Badge } from '@/components/ui'
import { Plus, Trash2, GitMerge, RefreshCw, ChevronRight, Save, AlertCircle, Link } from 'lucide-vue-next' import { Plus, Trash2, GitMerge, RefreshCw, ChevronRight, Save, AlertCircle, Link } from 'lucide-vue-next'
import { updateGlobalModel, getGlobalModel, getGlobalModelRoutingPreview } from '@/api/global-models' import { updateGlobalModel, getGlobalModel, getGlobalModelRoutingPreview } from '@/api/global-models'
@@ -241,6 +241,8 @@ const props = defineProps<{
modelName: string modelName: string
mappings: string[] mappings: string[]
loading?: boolean loading?: boolean
routingData?: ModelRoutingPreviewResponse | null // 外部传入的 routing 数据
loadingPreview?: boolean // 外部传入的加载状态
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
update: [mappings: string[]] update: [mappings: string[]]
@@ -267,9 +269,13 @@ const mappingValidations = computed<ValidationResult[]>(() => {
}) })
}) })
// 匹配预览状态 // 匹配预览状态 - 优先使用外部传入的数据
const loadingPreview = ref(false) const internalLoadingPreview = ref(false)
const routingData = ref<ModelRoutingPreviewResponse | null>(null) const internalRoutingData = ref<ModelRoutingPreviewResponse | null>(null)
// 计算属性:优先使用外部传入的数据
const loadingPreview = computed(() => props.loadingPreview ?? internalLoadingPreview.value)
const routingData = computed(() => props.routingData ?? internalRoutingData.value)
const REGEX_CACHE_MAX_SIZE = 100 const REGEX_CACHE_MAX_SIZE = 100
const regexCache = createLRURegexCache(REGEX_CACHE_MAX_SIZE) const regexCache = createLRURegexCache(REGEX_CACHE_MAX_SIZE)
@@ -542,22 +548,31 @@ async function saveMappings() {
} }
} }
// 刷新数据(通知父组件刷新,或在无外部数据时自行加载)
async function loadMatchPreview() { async function loadMatchPreview() {
// 如果有外部传入的数据,通知父组件刷新
if (props.routingData !== undefined) {
emit('refresh')
return
}
// 清空正则缓存,确保使用最新数据 // 清空正则缓存,确保使用最新数据
regexCache.clear() regexCache.clear()
matchCountCache.clear() matchCountCache.clear()
loadingPreview.value = true internalLoadingPreview.value = true
try { try {
routingData.value = await getGlobalModelRoutingPreview(props.globalModelId) internalRoutingData.value = await getGlobalModelRoutingPreview(props.globalModelId)
} catch (err) { } catch (err) {
log.error('加载匹配预览失败:', err) log.error('加载匹配预览失败:', err)
} finally { } finally {
loadingPreview.value = false internalLoadingPreview.value = false
} }
} }
onMounted(() => { // 监听外部 routingData 变化,清空缓存
loadMatchPreview() watch(() => props.routingData, () => {
regexCache.clear()
matchCountCache.clear()
}) })
// 组件卸载时清理缓存,防止内存泄漏 // 组件卸载时清理缓存,防止内存泄漏

View File

@@ -611,6 +611,9 @@ import { MAX_MODEL_NAME_LENGTH, createLRURegexCache, getCompiledModelMappingRege
const props = defineProps<{ const props = defineProps<{
globalModelId: string globalModelId: string
routingData?: ModelRoutingPreviewResponse | null
loading?: boolean
error?: string | null
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -624,9 +627,16 @@ const emit = defineEmits<{
const { success: showSuccess, error: showError } = useToast() const { success: showSuccess, error: showError } = useToast()
const { tick: countdownTick, start: startCountdownTimer } = useCountdownTimer() const { tick: countdownTick, start: startCountdownTimer } = useCountdownTimer()
const loading = ref(false) // 使用外部传入的数据或内部状态
const error = ref<string | null>(null) const internalRoutingData = ref<ModelRoutingPreviewResponse | null>(null)
const routingData = ref<ModelRoutingPreviewResponse | null>(null) const internalLoading = ref(false)
const internalError = ref<string | null>(null)
// 计算属性:优先使用外部传入的数据
const routingData = computed(() => props.routingData ?? internalRoutingData.value)
const loading = computed(() => props.loading ?? internalLoading.value)
const error = computed(() => props.error ?? internalError.value)
const modelMappingRegexCache = createLRURegexCache(200) const modelMappingRegexCache = createLRURegexCache(200)
const keyMatchedModelsCache = new Map<string, string[]>() const keyMatchedModelsCache = new Map<string, string[]>()
const compiledGlobalModelMappingRegexes = ref<RegExp[]>([]) const compiledGlobalModelMappingRegexes = ref<RegExp[]>([])
@@ -808,16 +818,22 @@ function toggleProviderInFormat(format: string, providerId: string, endpointId?:
} }
} }
// 加载数据 // 加载数据(仅在没有外部数据时使用)
async function loadRoutingData() { async function loadRoutingData() {
// 如果有外部传入的数据,通知父组件刷新
if (props.routingData !== undefined) {
emit('refresh')
return
}
if (!props.globalModelId) return if (!props.globalModelId) return
modelMappingRegexCache.clear() modelMappingRegexCache.clear()
keyMatchedModelsCache.clear() keyMatchedModelsCache.clear()
compiledGlobalModelMappingRegexes.value = [] compiledGlobalModelMappingRegexes.value = []
loading.value = true internalLoading.value = true
error.value = null internalError.value = null
try { try {
const data = await getGlobalModelRoutingPreview(props.globalModelId) const data = await getGlobalModelRoutingPreview(props.globalModelId)
@@ -828,15 +844,30 @@ async function loadRoutingData() {
if (regex) compiled.push(regex) if (regex) compiled.push(regex)
} }
routingData.value = data internalRoutingData.value = data
compiledGlobalModelMappingRegexes.value = compiled compiledGlobalModelMappingRegexes.value = compiled
} catch (err: any) { } catch (err: any) {
error.value = err.response?.data?.detail || '加载失败' internalError.value = err.response?.data?.detail || '加载失败'
} finally { } finally {
loading.value = false internalLoading.value = false
} }
} }
// 监听外部 routingData 变化,更新编译后的正则
watch(() => props.routingData, (data) => {
if (data) {
modelMappingRegexCache.clear()
keyMatchedModelsCache.clear()
const compiled: RegExp[] = []
for (const pattern of data.global_model_mappings || []) {
const regex = getCompiledModelMappingRegex(pattern, modelMappingRegexCache)
if (regex) compiled.push(regex)
}
compiledGlobalModelMappingRegexes.value = compiled
}
}, { immediate: true })
// 获取调度模式标签 // 获取调度模式标签
function getSchedulingModeLabel(mode: string): string { function getSchedulingModeLabel(mode: string): string {
const labels: Record<string, string> = { const labels: Record<string, string> = {
@@ -1086,9 +1117,9 @@ function getKeyProbeCountdown(key: RoutingKeyInfo): string {
async function handleRecoverKey(keyId: string, apiFormat: string) { async function handleRecoverKey(keyId: string, apiFormat: string) {
try { try {
const result = await recoverKeyHealth(keyId, apiFormat) const result = await recoverKeyHealth(keyId, apiFormat)
await loadRoutingData() // 通知父组件刷新数据
showSuccess(result.message || 'Key 已恢复')
emit('refresh') emit('refresh')
showSuccess(result.message || 'Key 已恢复')
} catch (err: any) { } catch (err: any) {
showError(err.response?.data?.detail || 'Key 恢复失败', '错误') showError(err.response?.data?.detail || 'Key 恢复失败', '错误')
} }
@@ -1098,11 +1129,17 @@ async function handleRecoverKey(keyId: string, apiFormat: string) {
watch(() => props.globalModelId, () => { watch(() => props.globalModelId, () => {
expandedFormats.value.clear() expandedFormats.value.clear()
expandedProvidersInFormat.value.clear() expandedProvidersInFormat.value.clear()
loadRoutingData() // 如果没有外部数据,才自己加载
if (props.routingData === undefined) {
loadRoutingData()
}
}, { immediate: false }) }, { immediate: false })
onMounted(() => { onMounted(() => {
loadRoutingData() // 如果没有外部数据,才自己加载
if (props.routingData === undefined) {
loadRoutingData()
}
startCountdownTimer() startCountdownTimer()
}) })

View File

@@ -876,8 +876,11 @@
:key="`models-${provider.id}`" :key="`models-${provider.id}`"
:provider="provider" :provider="provider"
:endpoints="endpoints" :endpoints="endpoints"
:models="providerModels"
:mapping-preview="providerMappingPreview"
@edit-model="handleEditModel" @edit-model="handleEditModel"
@batch-assign="handleBatchAssign" @batch-assign="handleBatchAssign"
@refresh="loadEndpoints"
/> />
<!-- 模型映射 --> <!-- 模型映射 -->
@@ -887,6 +890,9 @@
:key="`mapping-${provider.id}`" :key="`mapping-${provider.id}`"
:provider="provider" :provider="provider"
:provider-keys="providerKeys" :provider-keys="providerKeys"
:models="providerModels"
:mapping-preview="providerMappingPreview"
:endpoints="endpoints"
@refresh="handleModelMappingChanged" @refresh="handleModelMappingChanged"
/> />
</div> </div>
@@ -1027,7 +1033,14 @@ import { useToast } from '@/composables/useToast'
import { useConfirm } from '@/composables/useConfirm' import { useConfirm } from '@/composables/useConfirm'
import { useClipboard } from '@/composables/useClipboard' import { useClipboard } from '@/composables/useClipboard'
import { useCountdownTimer, formatCountdown, getOAuthExpiresCountdown } from '@/composables/useCountdownTimer' 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 { adminApi } from '@/api/admin'
import { import {
KeyFormDialog, KeyFormDialog,
@@ -1092,6 +1105,8 @@ const loading = ref(false)
const provider = ref<any>(null) const provider = ref<any>(null)
const endpoints = ref<ProviderEndpointWithKeys[]>([]) const endpoints = ref<ProviderEndpointWithKeys[]>([])
const providerKeys = ref<EndpointAPIKey[]>([]) // Provider 级别的 keys const providerKeys = ref<EndpointAPIKey[]>([]) // Provider 级别的 keys
const providerModels = ref<Model[]>([]) // Provider 级别的 models
const providerMappingPreview = ref<ProviderMappingPreviewResponse | null>(null) // 映射预览
// 系统级格式转换配置 // 系统级格式转换配置
const systemFormatConversionEnabled = ref(false) const systemFormatConversionEnabled = ref(false)
@@ -1632,21 +1647,29 @@ function formatKiroSubscription(title: string | undefined): string {
function shouldAutoRefreshCodexQuota(): boolean { function shouldAutoRefreshCodexQuota(): boolean {
if (provider.value?.provider_type !== 'codex') return false if (provider.value?.provider_type !== 'codex') return false
const now = Math.floor(Date.now() / 1000)
for (const { key } of allKeys.value) { for (const { key } of allKeys.value) {
if (!key.is_active) continue if (!key.is_active) continue
if (isTokenExpiringSoon(key, now)) return true
const meta: UpstreamMetadata | null | undefined = key.upstream_metadata const meta: UpstreamMetadata | null | undefined = key.upstream_metadata
// 只要有一个活跃 key 没有配额数据,就刷新一次 // 只要有一个活跃 key 没有配额数据,就刷新一次
if (!hasCodexQuotaData(meta)) { if (!hasCodexQuotaData(meta)) {
return true return true
} }
// 配额数据超过 5 分钟未更新,也触发刷新
const updatedAt = meta?.codex?.updated_at
if (typeof updatedAt !== 'number' || (now - updatedAt) > AUTO_QUOTA_REFRESH_STALE_SECONDS) {
return true
}
} }
return false return false
} }
// 检查 OAuth Token 是否即将过期Antigravity / Kiro // 检查 OAuth Token 是否即将过期(Codex / Antigravity / Kiro
function isTokenExpiringSoon(key: EndpointAPIKey, now: number): boolean { function isTokenExpiringSoon(key: EndpointAPIKey, now: number): boolean {
return key.oauth_invalid_at == null return key.oauth_invalid_at == null
&& typeof key.oauth_expires_at === 'number' && typeof key.oauth_expires_at === 'number'
@@ -1687,8 +1710,14 @@ function shouldAutoRefreshKiroQuota(): boolean {
if (isTokenExpiringSoon(key, now)) return true if (isTokenExpiringSoon(key, now)) return true
const meta = key.upstream_metadata
// 只要有一个活跃 key 没有配额数据,就刷新一次 // 只要有一个活跃 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 return true
} }
} }
@@ -2525,13 +2554,17 @@ async function loadEndpoints() {
if (!props.providerId) return if (!props.providerId) return
try { try {
// 并行加载端点列表Provider 级别的 keys // 并行加载端点列表Provider 级别的 keys、models 和映射预览
const [endpointsList, providerKeysResult] = await Promise.all([ const [endpointsList, providerKeysResult, modelsResult, mappingPreviewResult] = await Promise.all([
getProviderEndpoints(props.providerId), getProviderEndpoints(props.providerId),
getProviderKeys(props.providerId).catch(() => []), getProviderKeys(props.providerId).catch(() => []),
getProviderModels(props.providerId).catch(() => []),
getProviderMappingPreview(props.providerId).catch(() => null),
]) ])
providerKeys.value = providerKeysResult providerKeys.value = providerKeysResult
providerModels.value = modelsResult
providerMappingPreview.value = mappingPreviewResult
// 按 API 格式排序 // 按 API 格式排序
endpoints.value = endpointsList.sort((a, b) => { endpoints.value = endpointsList.sort((a, b) => {
const aIdx = API_FORMAT_ORDER.indexOf(a.api_format) const aIdx = API_FORMAT_ORDER.indexOf(a.api_format)

View File

@@ -343,7 +343,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue' import { ref, computed } from 'vue'
import { useSmartPagination } from '@/composables/useSmartPagination' import { useSmartPagination } from '@/composables/useSmartPagination'
import { Tag, Plus, Edit, Trash2, ChevronRight, Loader2, Play } from 'lucide-vue-next' import { Tag, Plus, Edit, Trash2, ChevronRight, Loader2, Play } from 'lucide-vue-next'
import { import {
@@ -354,15 +354,12 @@ import AlertDialog from '@/components/common/AlertDialog.vue'
import ModelMappingDialog, { type AliasGroup } from '../ModelMappingDialog.vue' import ModelMappingDialog, { type AliasGroup } from '../ModelMappingDialog.vue'
import { useToast } from '@/composables/useToast' import { useToast } from '@/composables/useToast'
import { import {
getProviderModels,
getProviderMappingPreview,
testModel, testModel,
type Model, type Model,
type ProviderModelAlias, type ProviderModelAlias,
type ProviderMappingPreviewResponse type ProviderMappingPreviewResponse
} from '@/api/endpoints' } from '@/api/endpoints'
import { getProviderEndpoints } from '@/api/endpoints/endpoints' import { type EndpointAPIKey } from '@/api/endpoints/keys'
import { getProviderKeys, type EndpointAPIKey } from '@/api/endpoints/keys'
import type { ProviderEndpoint } from '@/api/endpoints/types' import type { ProviderEndpoint } from '@/api/endpoints/types'
import { updateModel } from '@/api/endpoints/models' import { updateModel } from '@/api/endpoints/models'
import { parseTestModelError } from '@/utils/errorParser' import { parseTestModelError } from '@/utils/errorParser'
@@ -394,6 +391,9 @@ interface CombinedMapping {
const props = defineProps<{ const props = defineProps<{
provider: any provider: any
providerKeys?: EndpointAPIKey[] providerKeys?: EndpointAPIKey[]
models?: Model[]
mappingPreview?: ProviderMappingPreviewResponse | null
endpoints?: ProviderEndpoint[]
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -404,8 +404,6 @@ const { error: showError, success: showSuccess } = useToast()
// 状态 // 状态
const loading = ref(false) const loading = ref(false)
const models = ref<Model[]>([])
const aliasMappingPreview = ref<ProviderMappingPreviewResponse | null>(null)
const dialogOpen = ref(false) const dialogOpen = ref(false)
const deleteConfirmOpen = ref(false) const deleteConfirmOpen = ref(false)
const editingGroup = ref<AliasGroup | null>(null) const editingGroup = ref<AliasGroup | null>(null)
@@ -413,22 +411,21 @@ const deletingGroup = ref<AliasGroup | null>(null)
const testingMapping = ref<string | null>(null) const testingMapping = ref<string | null>(null)
const preselectedModelId = 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>>({}) 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()) const expandedItems = ref<Set<string>>(new Set())
// 是否有 key 配置了自动获取上游模型 // 是否有 key 配置了自动获取上游模型
const hasAutoFetchKey = computed(() => { const hasAutoFetchKey = computed(() => {
const keys = props.providerKeys || providerKeysState.value return providerKeysState.value.some(k => k.auto_fetch_models)
return keys.some(k => k.auto_fetch_models)
}) })
// 生成作用域唯一键 // 生成作用域唯一键
@@ -562,25 +559,9 @@ const {
paginatedItems: paginatedMappings, paginatedItems: paginatedMappings,
} = useSmartPagination(combinedMappings, mappingsListRef) } = useSmartPagination(combinedMappings, mappingsListRef)
// 加载数据 // 刷新数据(通知父组件刷新)
async function loadData() { function refresh() {
try { emit('refresh')
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
}
} }
// 删除确认描述 // 删除确认描述
@@ -653,7 +634,6 @@ async function confirmDelete() {
showSuccess('映射已删除') showSuccess('映射已删除')
deleteConfirmOpen.value = false deleteConfirmOpen.value = false
deletingGroup.value = null deletingGroup.value = null
await loadData()
emit('refresh') emit('refresh')
} catch (err: any) { } catch (err: any) {
showError(err.response?.data?.detail || '删除失败', '错误') showError(err.response?.data?.detail || '删除失败', '错误')
@@ -662,7 +642,6 @@ async function confirmDelete() {
// 对话框保存后回调 // 对话框保存后回调
async function onDialogSaved() { async function onDialogSaved() {
await loadData()
emit('refresh') 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({ defineExpose({
dialogOpen: computed(() => dialogOpen.value || deleteConfirmOpen.value), dialogOpen: computed(() => dialogOpen.value || deleteConfirmOpen.value),
reload: loadData reload: refresh
}) })
</script> </script>

View File

@@ -246,7 +246,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed } from 'vue'
import { useSmartPagination } from '@/composables/useSmartPagination' import { useSmartPagination } from '@/composables/useSmartPagination'
import { Box, Edit, Layers, Power, Copy, Loader2, Play } from 'lucide-vue-next' import { Box, Edit, Layers, Power, Copy, Loader2, Play } from 'lucide-vue-next'
import Card from '@/components/ui/card.vue' import Card from '@/components/ui/card.vue'
@@ -261,8 +261,6 @@ import { useToast } from '@/composables/useToast'
import { useClipboard } from '@/composables/useClipboard' import { useClipboard } from '@/composables/useClipboard'
import { sortResolutionEntries } from '@/utils/form' import { sortResolutionEntries } from '@/utils/form'
import { import {
getProviderModels,
getProviderMappingPreview,
testModel, testModel,
type Model, type Model,
type ProviderMappingPreviewResponse type ProviderMappingPreviewResponse
@@ -280,11 +278,14 @@ interface Endpoint {
const props = defineProps<{ const props = defineProps<{
provider: any provider: any
endpoints?: Endpoint[] endpoints?: Endpoint[]
models?: Model[]
mappingPreview?: ProviderMappingPreviewResponse | null
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
'editModel': [model: Model] 'editModel': [model: Model]
'batchAssign': [] 'batchAssign': []
'refresh': []
}>() }>()
const { error: showError, success: showSuccess } = useToast() const { error: showError, success: showSuccess } = useToast()
@@ -292,12 +293,16 @@ const { copyToClipboard } = useClipboard()
// 状态 // 状态
const loading = ref(false) const loading = ref(false)
const models = ref<Model[]>([]) const localModels = ref<Model[]>([])
const mappingPreview = ref<ProviderMappingPreviewResponse | null>(null) const localMappingPreview = ref<ProviderMappingPreviewResponse | null>(null)
const togglingModelId = ref<string | null>(null) const togglingModelId = ref<string | null>(null)
const testingModelId = ref<string | null>(null) const testingModelId = ref<string | null>(null)
const formatMenuOpen = ref<Record<string, boolean>>({}) const formatMenuOpen = ref<Record<string, boolean>>({})
// 使用 props 传入的数据,或使用本地数据
const models = computed(() => props.models ?? localModels.value)
const mappingPreview = computed(() => props.mappingPreview ?? localMappingPreview.value)
// 获取可用的 API 格式(有活跃端点且有活跃 Key // 获取可用的 API 格式(有活跃端点且有活跃 Key
const availableApiFormats = computed(() => { const availableApiFormats = computed(() => {
if (!props.endpoints) return [] if (!props.endpoints) return []
@@ -329,24 +334,9 @@ async function copyModelId(modelId: string) {
await copyToClipboard(modelId) await copyToClipboard(modelId)
} }
// 加载模型和映射预览 // 刷新数据(通知父组件刷新)
async function loadModels() { function refresh() {
try { emit('refresh')
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
}
} }
// 格式化价格显示 // 格式化价格显示
@@ -547,12 +537,8 @@ async function testModelConnection(model: Model, apiFormat?: string) {
} }
} }
onMounted(() => {
loadModels()
})
// 暴露给父组件 // 暴露给父组件
defineExpose({ defineExpose({
reload: loadModels reload: refresh
}) })
</script> </script>

View File

@@ -1282,7 +1282,6 @@ onMounted(async () => {
} }
await Promise.all([ await Promise.all([
loadDashboardData(), loadDashboardData(),
loadDailyStats(),
loadAnnouncements() loadAnnouncements()
]) ])
await nextTick() await nextTick()