2025-12-07 17:17:20 +08:00
|
|
|
|
<template>
|
2026-04-26 18:51:38 +08:00
|
|
|
|
<a-dropdown trigger="click" @open-change="handleOpenChange">
|
|
|
|
|
|
<div class="model-select" :class="modelSelectClasses" @click.prevent>
|
|
|
|
|
|
<div class="model-select-content">
|
|
|
|
|
|
<div class="model-info">
|
|
|
|
|
|
<a-tooltip :title="displayText" placement="right">
|
|
|
|
|
|
<span class="model-text">{{ displayText }}</span>
|
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
</div>
|
2025-12-07 17:17:20 +08:00
|
|
|
|
</div>
|
2026-04-26 18:51:38 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template #overlay>
|
|
|
|
|
|
<a-menu class="scrollable-menu">
|
|
|
|
|
|
<!-- V2 模型列表(按 provider 分组) -->
|
|
|
|
|
|
<a-menu-item-group v-for="(providerData, providerId) in v2Models" :key="`v2-${providerId}`">
|
|
|
|
|
|
<template #title>
|
|
|
|
|
|
<span>{{ providerId }}</span>
|
|
|
|
|
|
<a-tag color="success" size="small" class="provider-tag">新</a-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<a-menu-item
|
|
|
|
|
|
v-for="model in providerData.models"
|
|
|
|
|
|
:key="model.spec"
|
|
|
|
|
|
@click="handleSelect(model.spec)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="model-option">
|
|
|
|
|
|
<span class="model-option-name">
|
|
|
|
|
|
{{ model.display_name }}
|
|
|
|
|
|
<span class="model-dimension">({{ model.dimension }})</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span
|
|
|
|
|
|
class="model-status-icon"
|
|
|
|
|
|
:class="getV2StatusClass(model.spec)"
|
|
|
|
|
|
:title="getV2StatusTooltip(model.spec)"
|
|
|
|
|
|
>{{ getV2StatusIcon(model.spec) }}</span
|
|
|
|
|
|
>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-menu-item>
|
|
|
|
|
|
</a-menu-item-group>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- V1 模型列表(过时,仅保留兼容) -->
|
|
|
|
|
|
<a-menu-item-group v-if="v1EmbedModels.length" key="v1" title="V1 版本(过时)">
|
|
|
|
|
|
<a-menu-item v-for="name in v1EmbedModels" :key="name" @click="handleSelect(name)">
|
|
|
|
|
|
<div class="model-option">
|
|
|
|
|
|
<span class="model-option-name">
|
|
|
|
|
|
{{ name }}
|
|
|
|
|
|
<span class="model-dimension"
|
|
|
|
|
|
>({{ configStore.config?.embed_model_names[name]?.dimension }})</span
|
|
|
|
|
|
>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<a-tag color="default" size="small" class="provider-tag">过时</a-tag>
|
|
|
|
|
|
<span
|
|
|
|
|
|
class="model-status-icon"
|
|
|
|
|
|
:class="getV1StatusClass(name)"
|
|
|
|
|
|
:title="getV1StatusTooltip(name)"
|
|
|
|
|
|
>{{ getV1StatusIcon(name) }}</span
|
|
|
|
|
|
>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-menu-item>
|
|
|
|
|
|
</a-menu-item-group>
|
|
|
|
|
|
</a-menu>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-dropdown>
|
2025-12-07 17:17:20 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-04-26 18:51:38 +08:00
|
|
|
|
import { computed, reactive, ref } from 'vue'
|
2025-12-07 17:17:20 +08:00
|
|
|
|
import { useConfigStore } from '@/stores/config'
|
|
|
|
|
|
import { embeddingApi } from '@/apis/knowledge_api'
|
2026-04-26 18:51:38 +08:00
|
|
|
|
import { modelProviderApi } from '@/apis/system_api'
|
2025-12-07 17:17:20 +08:00
|
|
|
|
import { message } from 'ant-design-vue'
|
2026-04-26 19:59:15 +08:00
|
|
|
|
import { useModelStatus } from '@/composables/useModelStatus'
|
2025-12-07 17:17:20 +08:00
|
|
|
|
|
|
|
|
|
|
const configStore = useConfigStore()
|
|
|
|
|
|
|
2026-04-26 18:51:38 +08:00
|
|
|
|
const props = defineProps({
|
2025-12-07 17:17:20 +08:00
|
|
|
|
value: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ''
|
|
|
|
|
|
},
|
|
|
|
|
|
placeholder: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: '请选择嵌入模型'
|
|
|
|
|
|
},
|
2026-04-26 18:51:38 +08:00
|
|
|
|
size: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: 'small',
|
|
|
|
|
|
validator: (value) => ['default', 'small', 'middle', 'large'].includes(value)
|
|
|
|
|
|
},
|
2025-12-07 17:17:20 +08:00
|
|
|
|
style: {
|
|
|
|
|
|
type: Object,
|
2026-04-26 18:51:38 +08:00
|
|
|
|
default: () => ({ width: '100%' })
|
2025-12-07 17:17:20 +08:00
|
|
|
|
},
|
|
|
|
|
|
disabled: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits(['update:value', 'change'])
|
|
|
|
|
|
|
2026-04-26 18:51:38 +08:00
|
|
|
|
const v2Models = ref({})
|
2026-04-26 19:59:15 +08:00
|
|
|
|
const v1ModelStatuses = reactive({})
|
|
|
|
|
|
const { statusMap: v2ModelStatuses, getStatusIcon: getV2StatusIcon, getStatusClass: getV2StatusClass, getStatusTooltip: getV2StatusTooltip, checkV2Status, checkV2Statuses } = useModelStatus()
|
2025-12-07 17:17:20 +08:00
|
|
|
|
const state = reactive({
|
|
|
|
|
|
checkingStatus: false
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-04-26 18:51:38 +08:00
|
|
|
|
const displayText = computed(() => props.value || props.placeholder)
|
|
|
|
|
|
|
|
|
|
|
|
const resolvedSize = computed(() => props.size || 'small')
|
|
|
|
|
|
const modelSelectClasses = computed(() => ({
|
|
|
|
|
|
'model-select--middle': resolvedSize.value === 'middle',
|
|
|
|
|
|
'model-select--large': resolvedSize.value === 'large'
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
const v1EmbedModels = computed(() => {
|
|
|
|
|
|
return Object.keys(configStore?.config?.embed_model_names || {})
|
2025-12-07 17:17:20 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-04-26 18:51:38 +08:00
|
|
|
|
const handleOpenChange = async (open) => {
|
|
|
|
|
|
if (!open) return
|
|
|
|
|
|
fetchV2Models()
|
|
|
|
|
|
checkV1ModelStatuses()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const fetchV2Models = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await modelProviderApi.getV2Models('embedding')
|
|
|
|
|
|
if (response.success) {
|
|
|
|
|
|
v2Models.value = response.data || {}
|
|
|
|
|
|
// 拉取到 V2 模型列表后,逐一检查状态
|
|
|
|
|
|
await checkV2ModelStatuses()
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取 V2 embedding 模型失败:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const checkV2ModelStatuses = async () => {
|
|
|
|
|
|
state.checkingStatus = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
for (const providerData of Object.values(v2Models.value)) {
|
2026-04-26 19:59:15 +08:00
|
|
|
|
await checkV2Statuses(providerData.models || [])
|
2026-04-26 18:51:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('检查 V2 模型状态失败:', error)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
state.checkingStatus = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const checkV1ModelStatuses = async () => {
|
2025-12-07 17:17:20 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await embeddingApi.getAllModelsStatus()
|
|
|
|
|
|
if (response.status.models) {
|
2026-04-26 19:59:15 +08:00
|
|
|
|
Object.assign(v1ModelStatuses, response.status.models)
|
2025-12-07 17:17:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-04-26 18:51:38 +08:00
|
|
|
|
console.error('检查 V1 模型状态失败:', error)
|
2025-12-07 17:17:20 +08:00
|
|
|
|
message.error('获取模型状态失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-26 18:51:38 +08:00
|
|
|
|
// V1 模型状态辅助函数
|
|
|
|
|
|
const getV1StatusIcon = (modelId) => {
|
2026-04-26 19:59:15 +08:00
|
|
|
|
const status = v1ModelStatuses[modelId]
|
2025-12-07 17:17:20 +08:00
|
|
|
|
if (!status) return '○'
|
|
|
|
|
|
if (status.status === 'available') return '✓'
|
|
|
|
|
|
if (status.status === 'unavailable') return '✗'
|
|
|
|
|
|
if (status.status === 'error') return '⚠'
|
|
|
|
|
|
return '○'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-26 18:51:38 +08:00
|
|
|
|
const getV1StatusClass = (modelId) => {
|
2026-04-26 19:59:15 +08:00
|
|
|
|
const status = v1ModelStatuses[modelId]
|
2026-04-26 18:51:38 +08:00
|
|
|
|
return status?.status || ''
|
2025-12-07 17:17:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-26 18:51:38 +08:00
|
|
|
|
const getV1StatusTooltip = (modelId) => {
|
2026-04-26 19:59:15 +08:00
|
|
|
|
const status = v1ModelStatuses[modelId]
|
2026-04-26 18:51:38 +08:00
|
|
|
|
if (!status) return '状态未知'
|
|
|
|
|
|
let statusText =
|
|
|
|
|
|
{ available: '可用', unavailable: '不可用', error: '错误' }[status.status] || '未知'
|
|
|
|
|
|
return `${statusText}: ${status.message || '无详细信息'}`
|
2025-12-07 17:17:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleSelect = (value) => {
|
|
|
|
|
|
emit('update:value', value)
|
|
|
|
|
|
emit('change', value)
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="less" scoped>
|
2026-04-26 18:51:38 +08:00
|
|
|
|
@import '@/assets/css/model-selector-common.less';
|
|
|
|
|
|
|
|
|
|
|
|
.model-option {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.model-option-name {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.model-dimension {
|
|
|
|
|
|
color: var(--gray-500);
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
margin-left: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.model-status-icon {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
color: var(--gray-500);
|
|
|
|
|
|
|
|
|
|
|
|
&.available {
|
|
|
|
|
|
color: var(--color-success-500);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.unavailable {
|
|
|
|
|
|
color: var(--color-error-500);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.error {
|
|
|
|
|
|
color: var(--color-warning-500);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
</style>
|