import { computed, ref } from 'vue' import { testModelConnectivity } from '../../services/settings.js' import { useToast } from '../../composables/useToast.js' import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue' import { CUSTOM_OPENAI_PROVIDER, MODEL_SECRET_MASK, MODEL_TYPE_LABELS, MODEL_TYPE_OPTIONS, getProviderEndpoint, getRerankerEndpoint, isModelSecretMask, normalizeLlmModelRows, normalizeProviderValue, normalizeValue } from '../../utils/settingsModelHelper.js' const FIXED_MODEL_SLOTS = new Set(['main', 'backup', 'embedding', 'reranker']) const MODEL_TYPE_CAPABILITY = Object.fromEntries( MODEL_TYPE_OPTIONS.map((option) => [option.value, option.capability]) ) function buildEmptyModelDraft() { return { slot: '', provider: CUSTOM_OPENAI_PROVIDER, url: '', apiKey: '', apiKeyConfigured: false, modelId: '', type: 'llm' } } function generateModelSlot(type) { const prefix = type === 'embedding' ? 'embedding' : type === 'rerank' ? 'rerank' : 'llm' const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}` return `${prefix}_${suffix}` } function normalizeDraftModel(draft) { return normalizeLlmModelRows([ { slot: draft.slot || generateModelSlot(draft.type), provider: draft.provider, url: draft.url, apiKey: draft.apiKey, apiKeyConfigured: draft.apiKeyConfigured, modelId: draft.modelId, type: draft.type } ])[0] } export default { name: 'LlmSettingsPanel', components: { EnterpriseSelect }, props: { llmForm: { type: Object, required: true }, providerOptions: { type: Array, required: true } }, setup(props) { const { toast } = useToast() const modelTestState = ref({}) const modelDialogOpen = ref(false) const editingSlot = ref('') const modelDraft = ref(buildEmptyModelDraft()) const modelRows = computed(() => normalizeLlmModelRows(props.llmForm.models)) const isEditingFixedModel = computed(() => isFixedModelSlot(editingSlot.value)) function replaceModelRows(rows) { props.llmForm.models = normalizeLlmModelRows(rows) } function getModelTypeLabel(type) { return MODEL_TYPE_LABELS[type] || MODEL_TYPE_LABELS.llm } function isFixedModelSlot(slot) { return FIXED_MODEL_SLOTS.has(String(slot || '')) } function getModelTestState(slot) { return modelTestState.value[slot] || { status: 'idle', message: '' } } function isModelTesting(slot) { return getModelTestState(slot).status === 'testing' } function openAddModelDialog() { editingSlot.value = '' modelDraft.value = buildEmptyModelDraft() modelDialogOpen.value = true } function openEditModelDialog(model) { editingSlot.value = model.slot modelDraft.value = { ...model } modelDialogOpen.value = true } function closeModelDialog() { modelDialogOpen.value = false editingSlot.value = '' modelDraft.value = buildEmptyModelDraft() } function applyProviderPresetToDraft() { const provider = normalizeProviderValue(modelDraft.value.provider, CUSTOM_OPENAI_PROVIDER) modelDraft.value.provider = provider modelDraft.value.url = modelDraft.value.type === 'rerank' ? getRerankerEndpoint(provider) : getProviderEndpoint(provider) } function selectDraftModelType(type) { if (isEditingFixedModel.value) { return } modelDraft.value.type = type applyProviderPresetToDraft() } function clearDraftSecretMask() { if (isModelSecretMask(modelDraft.value.apiKey)) { modelDraft.value.apiKey = '' } } function validateDraftModel() { const provider = normalizeValue(modelDraft.value.provider) const url = normalizeValue(modelDraft.value.url) const modelId = normalizeValue(modelDraft.value.modelId) if (!provider || !url || !modelId) { toast('请完整填写供应商、接口地址和 model_id。') return false } return true } function saveModelDialog() { if (!validateDraftModel()) { return } const nextModel = normalizeDraftModel(modelDraft.value) const rows = [...modelRows.value] const currentIndex = rows.findIndex((model) => model.slot === editingSlot.value) if (currentIndex >= 0) { rows.splice(currentIndex, 1, nextModel) } else { rows.push(nextModel) } replaceModelRows(rows) closeModelDialog() } function removeModel(model) { if (isFixedModelSlot(model.slot)) { toast('内置运行时槽位不能删除。') return } if (typeof window !== 'undefined' && !window.confirm('确定删除这个模型配置吗?')) { return } replaceModelRows(modelRows.value.filter((row) => row.slot !== model.slot)) } async function testModelConnection(model) { if (!normalizeValue(model.provider) || !normalizeValue(model.modelId) || !normalizeValue(model.url)) { const message = '请先完整填写模型的供应商、model_id 和接口地址。' modelTestState.value[model.slot] = { status: 'error', message } toast(message) return } modelTestState.value[model.slot] = { status: 'testing', message: '正在测试模型连通性...' } const payload = { provider: model.provider, model: model.modelId, endpoint: model.url, api_key: model.apiKey === MODEL_SECRET_MASK ? '' : model.apiKey, capability: MODEL_TYPE_CAPABILITY[model.type] || 'chat', slot: model.slot } try { const result = await testModelConnectivity(payload) modelTestState.value[model.slot] = { status: result.ok ? 'success' : 'error', message: result.detail || (result.ok ? '模型连接成功。' : '模型连接失败。') } toast(modelTestState.value[model.slot].message) } catch (error) { const message = error.message || '模型测试请求失败,请确认 FastAPI 已启动。' modelTestState.value[model.slot] = { status: 'error', message } toast(message) } } return { MODEL_TYPE_OPTIONS, applyProviderPresetToDraft, clearDraftSecretMask, closeModelDialog, getModelTestState, getModelTypeLabel, isEditingFixedModel, isFixedModelSlot, isModelTesting, modelDialogOpen, modelDraft, modelRows, openAddModelDialog, openEditModelDialog, removeModel, saveModelDialog, selectDraftModelType, testModelConnection } } }