merge(main): 解决 usage 展示与生命周期同步冲突

This commit is contained in:
MMEXA
2026-07-18 05:38:38 +08:00
111 changed files with 8984 additions and 1715 deletions
@@ -782,7 +782,7 @@
:models="providerModels"
:endpoints="endpoints"
:provider-keys="providerKeys"
:loading="loadingProviderModels || loadingProviderKeys"
:loading="loadingProviderModels"
@edit-model="handleEditModel"
@batch-assign="handleBatchAssign"
@refresh="loadEndpoints"
@@ -798,7 +798,7 @@
:provider-keys="providerKeys"
:models="providerModels"
:mapping-preview="providerMappingPreview"
:loading="loadingProviderEndpoints || loadingProviderKeys || loadingProviderModels || loadingProviderMappingPreview"
:loading="loadingProviderMappingPreview"
@refresh="handleModelMappingChanged"
/>
</div>
@@ -1303,8 +1303,6 @@ watch(
loading.value = false
}
void loadSystemFormatConversionConfig()
// mapping-preview 较慢,不阻塞首屏渲染
void loadMappingPreview()
if (!hasInitialProvider) {
await loadProvider()
}
@@ -1313,7 +1311,13 @@ watch(
if (newOpen && !oldOpen) {
startCountdownTimer()
}
void endpointsPromise.then(() => autoRefreshQuotaInBackground())
// 优先完成端点、密钥和模型的首屏数据,再请求计算量较大的映射预览。
// 同时校验抽屉状态,避免关闭或切换 Provider 后启动无用请求。
void endpointsPromise.then(() => {
if (!props.open || props.providerId !== newId) return
void loadMappingPreview()
void autoRefreshQuotaInBackground()
})
} else if (!newOpen && oldOpen) {
// 使在途请求失效,避免关闭后旧响应回写
providerLoadRequestId += 1
@@ -0,0 +1,38 @@
import { describe, expect, it } from 'vitest'
import { createSSRApp, h } from 'vue'
import { renderToString } from '@vue/server-renderer'
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
import ModelMappingTab from '../provider-tabs/ModelMappingTab.vue'
const provider = {
id: 'provider-demo',
name: 'Demo Provider',
provider_type: 'custom',
is_active: true,
active_keys: 0,
api_formats: [],
} as ProviderWithEndpointsSummary
describe('ModelMappingTab response contracts', () => {
it('keeps the module visible when a legacy or malformed preview reaches the component', async () => {
const app = createSSRApp({
render: () => h(ModelMappingTab, {
provider,
models: [],
endpoints: [],
providerKeys: [],
mappingPreview: {
message: '演示模式:该接口暂未模拟',
demo_mode: true,
},
loading: false,
}),
})
const html = await renderToString(app)
expect(html).toContain('模型映射')
expect(html).toContain('暂无模型映射')
})
})
@@ -0,0 +1,32 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
const source = readFileSync(
resolve(process.cwd(), 'src/features/providers/components/ProviderDetailDrawer.vue'),
'utf8',
)
describe('ProviderDetailDrawer loading priorities', () => {
it('loads mapping preview after first-screen provider data', () => {
const openWatcher = source
.split('// 合并监听 providerId 和 open')[1]
?.split('} else if (!newOpen && oldOpen)')[0]
expect(openWatcher).toBeTruthy()
expect(openWatcher).toContain('const endpointsPromise = loadEndpoints()')
expect(openWatcher).toContain('endpointsPromise.then(() => {')
expect(openWatcher).toContain('if (!props.open || props.providerId !== newId) return')
expect(openWatcher).toContain('void loadMappingPreview()')
const beforeEndpoints = openWatcher?.split('const endpointsPromise = loadEndpoints()')[0]
expect(beforeEndpoints).not.toContain('loadMappingPreview()')
})
it('keeps model and mapping loading states independent', () => {
expect(source).toContain(':loading="loadingProviderModels"')
expect(source).toContain(':loading="loadingProviderMappingPreview"')
expect(source).not.toContain(':loading="loadingProviderModels || loadingProviderKeys"')
expect(source).not.toContain(':loading="loadingProviderEndpoints || loadingProviderKeys || loadingProviderModels || loadingProviderMappingPreview"')
})
})
@@ -599,13 +599,20 @@ const regexMappings = computed<CombinedMapping[]>(() => {
const result: CombinedMapping[] = []
const modelMap = new Map<string, CombinedMapping>()
for (const keyInfo of aliasMappingPreview.value.keys) {
for (const gm of keyInfo.matching_global_models) {
const previewKeys = Array.isArray(aliasMappingPreview.value.keys)
? aliasMappingPreview.value.keys
: []
for (const keyInfo of previewKeys) {
const matchingGlobalModels = Array.isArray(keyInfo.matching_global_models)
? keyInfo.matching_global_models
: []
for (const gm of matchingGlobalModels) {
const matchedModels = Array.isArray(gm.matched_models) ? gm.matched_models : []
if (!modelMap.has(gm.global_model_id)) {
modelMap.set(gm.global_model_id, {
key: `regex-${gm.global_model_id}`,
type: 'regex',
targetModelName: gm.display_name,
targetModelName: gm.display_name || gm.global_model_name || gm.global_model_id,
targetModelId: gm.global_model_id,
globalModelName: gm.global_model_name,
mappings: [],
@@ -618,7 +625,7 @@ const regexMappings = computed<CombinedMapping[]>(() => {
if (!mapping) continue
// 添加 Key 信息
const keyMatches: MappingItem[] = gm.matched_models.map(m => ({
const keyMatches: MappingItem[] = matchedModels.map(m => ({
name: m.allowed_model,
pattern: m.mapping_pattern
}))
@@ -631,7 +638,7 @@ const regexMappings = computed<CombinedMapping[]>(() => {
})
// 收集所有映射(去重)
for (const match of gm.matched_models) {
for (const match of matchedModels) {
if (!mapping.mappings.some(m => m.name === match.allowed_model)) {
mapping.mappings.push({
name: match.allowed_model,