- useThemeSkin 重构主题皮肤应用逻辑,支持企业 AI 风格主题切换 - settingsModelHelper 新增主题与模型表字段映射,useSettings 适配 - LlmSettingsPanel/SettingsView 面板重构,支持多模型行编辑与主题区块 - settings-view.css 适配主题样式,新增 settings-theme-section 测试,更新 settings-llm-section 测试
240 lines
6.6 KiB
JavaScript
240 lines
6.6 KiB
JavaScript
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
|
|
}
|
|
}
|
|
}
|