fix(ui):优化模型管理器组件结构和功能
- 将文本模型和图像模型管理分离到独立组件 - 更新图像模型编辑模态框的自动完成属性 -重构模型管理器UI布局和交互逻辑 - 移除冗余的SVG图标重复代码- 改进模型管理器的标签页切换功能 -优化模型管理器的依赖注入和事件处理 - 简化模型管理器的代码结构提高可维护性
This commit is contained in:
1 parent
eced9ca198
commit
e7f3d23454
10 files changed
+1585
-1710
No files matched your search
@@ -45,7 +45,7 @@
|
||||
:type="field.name.toLowerCase().includes('key') ? 'password' : 'text'"
|
||||
:placeholder="field.placeholder"
|
||||
:required="field.required"
|
||||
:autocomplete="field.name.toLowerCase().includes('key') ? 'off' : 'on'"
|
||||
:autocomplete="field.name.toLowerCase().includes('key') ? 'new-password' : 'on'"
|
||||
@update:value="onConnectionConfigChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,422 @@
|
||||
<template>
|
||||
<NModal
|
||||
:show="show"
|
||||
preset="card"
|
||||
:style="{ width: '90vw', maxWidth: '1000px', maxHeight: '90vh' }"
|
||||
:title="modalTitle.value"
|
||||
size="large"
|
||||
:bordered="false"
|
||||
:segmented="true"
|
||||
@update:show="handleUpdateShow"
|
||||
>
|
||||
<NScrollbar v-if="formReady" style="max-height: 75vh;">
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<NForm label-placement="left" label-width="auto" size="small">
|
||||
<NFormItem v-if="!isEditing" :label="t('modelManager.modelKey')">
|
||||
<NInput
|
||||
v-model:value="form.id"
|
||||
:placeholder="t('modelManager.modelKeyPlaceholder')"
|
||||
required
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="t('modelManager.displayName')">
|
||||
<NInput
|
||||
v-model:value="form.name"
|
||||
:placeholder="t('modelManager.displayNamePlaceholder')"
|
||||
required
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NDivider style="margin: 12px 0 8px 0;" />
|
||||
<NH4 style="margin: 0 0 12px 0; font-size: 14px;">提供商配置</NH4>
|
||||
|
||||
<NFormItem label="提供商">
|
||||
<NSelect
|
||||
v-model:value="form.providerId"
|
||||
:options="providerOptions"
|
||||
:loading="isLoadingProviders"
|
||||
placeholder="选择提供商"
|
||||
@update:value="onProviderChange"
|
||||
required
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem
|
||||
v-for="field in connectionFields"
|
||||
:key="field.name"
|
||||
:label="field.name === 'apiKey' ? t('modelManager.apiKey') : (field.name === 'baseURL' ? t('modelManager.apiUrl') : field.name)"
|
||||
>
|
||||
<template v-if="field.name === 'baseURL'" #label>
|
||||
<NSpace align="center" :size="4">
|
||||
<span>{{ t('modelManager.apiUrl') }}</span>
|
||||
<NText depth="3" :title="t('modelManager.apiUrlHint')" style="cursor: help;">?</NText>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<template v-if="field.type === 'string'">
|
||||
<NInput
|
||||
v-model:value="form.connectionConfig[field.name]"
|
||||
:type="field.name.toLowerCase().includes('key') ? 'password' : 'text'"
|
||||
:placeholder="field.placeholder"
|
||||
:required="field.required"
|
||||
:autocomplete="field.name.toLowerCase().includes('key') ? 'new-password' : 'on'"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="field.type === 'number'">
|
||||
<NInputNumber
|
||||
v-model:value="form.connectionConfig[field.name]"
|
||||
:placeholder="field.placeholder"
|
||||
:required="field.required"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="field.type === 'boolean'">
|
||||
<NCheckbox v-model:checked="form.connectionConfig[field.name]">
|
||||
{{ field.name }}
|
||||
</NCheckbox>
|
||||
</template>
|
||||
</NFormItem>
|
||||
|
||||
<NDivider style="margin: 12px 0 8px 0;" />
|
||||
<NH4 style="margin: 0 0 12px 0; font-size: 14px;">模型配置</NH4>
|
||||
|
||||
<NFormItem :label="t('modelManager.defaultModel')">
|
||||
<NSpace align="center" style="width: 100%;">
|
||||
<NSelect
|
||||
v-model:value="form.modelId"
|
||||
:options="modelOptions"
|
||||
:loading="isLoadingModelOptions"
|
||||
:placeholder="t('modelManager.defaultModelPlaceholder')"
|
||||
style="flex: 1; min-width: 200px;"
|
||||
clearable
|
||||
filterable
|
||||
required
|
||||
/>
|
||||
|
||||
<NTooltip :disabled="!canRefreshModelOptions" :show-arrow="false">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
@click="refreshModelOptions()"
|
||||
:loading="isLoadingModelOptions"
|
||||
:disabled="!canRefreshModelOptions"
|
||||
circle
|
||||
secondary
|
||||
type="primary"
|
||||
size="small"
|
||||
style="flex-shrink: 0;"
|
||||
>
|
||||
<template #icon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 14px; height: 14px;">
|
||||
<polyline points="23 4 23 10 17 10"/>
|
||||
<polyline points="1 20 1 14 7 14"/>
|
||||
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/>
|
||||
</svg>
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
{{ t('modelManager.clickToFetchModels') }}
|
||||
</NTooltip>
|
||||
</NSpace>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
|
||||
<NDivider style="margin: 12px 0 8px 0;" />
|
||||
<NSpace justify="space-between" align="center" style="margin-bottom: 12px;">
|
||||
<NSpace vertical :size="4">
|
||||
<NH4 style="margin: 0; font-size: 14px;">{{ t('modelManager.advancedParameters.title') }}</NH4>
|
||||
<NText depth="3" style="font-size: 12px;">
|
||||
{{ t('modelManager.advancedParameters.currentProvider') }}:
|
||||
<NText strong>
|
||||
{{ currentProviderType === 'custom' ? t('modelManager.advancedParameters.customProvider') : currentProviderType.toUpperCase() }}
|
||||
</NText>
|
||||
<span v-if="availableLLMParamDefinitions.length > 0">
|
||||
({{ availableLLMParamDefinitions.length }}{{ t('modelManager.advancedParameters.availableParams') }})
|
||||
</span>
|
||||
<NText v-else type="warning">
|
||||
({{ t('modelManager.advancedParameters.noAvailableParams') }})
|
||||
</NText>
|
||||
</NText>
|
||||
</NSpace>
|
||||
|
||||
<NSelect
|
||||
v-model:value="selectedNewLLMParamId"
|
||||
@update:value="handleQuickAddParam"
|
||||
style="width: 220px;"
|
||||
size="small"
|
||||
:options="[
|
||||
{ label: t('modelManager.advancedParameters.select'), value: '', disabled: true },
|
||||
...availableLLMParamDefinitions.map(paramDef => ({
|
||||
label: paramDef.labelKey ? t(paramDef.labelKey) : paramDef.name,
|
||||
value: paramDef.id
|
||||
})),
|
||||
{ label: t('modelManager.advancedParameters.custom'), value: 'custom' }
|
||||
]"
|
||||
/>
|
||||
</NSpace>
|
||||
|
||||
<NCard v-if="selectedNewLLMParamId === 'custom'" size="small" style="margin: 12px 0;">
|
||||
<template #header>
|
||||
<NSpace justify="space-between" align="center">
|
||||
<NText strong>{{ t('modelManager.advancedParameters.custom') }}</NText>
|
||||
<NButton @click="cancelCustomParam" size="tiny" quaternary circle>×</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
<NSpace vertical :size="12">
|
||||
<NSpace vertical :size="8">
|
||||
<NText depth="3" style="font-size: 12px;">参数名称</NText>
|
||||
<NInput
|
||||
v-model:value="customLLMParam.key"
|
||||
:placeholder="t('modelManager.advancedParameters.customKeyPlaceholder')"
|
||||
size="small"
|
||||
/>
|
||||
</NSpace>
|
||||
<NSpace vertical :size="8">
|
||||
<NText depth="3" style="font-size: 12px;">参数值</NText>
|
||||
<NInput
|
||||
v-model:value="customLLMParam.value"
|
||||
:placeholder="t('modelManager.advancedParameters.customValuePlaceholder')"
|
||||
size="small"
|
||||
/>
|
||||
</NSpace>
|
||||
<NSpace justify="end">
|
||||
<NButton @click="cancelCustomParam" size="small">{{ t('common.cancel') }}</NButton>
|
||||
<NButton
|
||||
@click="handleCustomParamAdd"
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="!customLLMParam.key || !customLLMParam.value"
|
||||
>
|
||||
{{ t('common.add') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
|
||||
<NText
|
||||
v-if="Object.keys(currentParamOverrides || {}).length === 0"
|
||||
depth="3"
|
||||
style="font-size: 14px; margin-bottom: 12px;"
|
||||
>
|
||||
{{ t('modelManager.advancedParameters.noParamsConfigured') }}
|
||||
</NText>
|
||||
|
||||
<NSpace vertical :size="12">
|
||||
<NCard
|
||||
v-for="(value, key) in currentParamOverrides"
|
||||
:key="key"
|
||||
size="small"
|
||||
embedded
|
||||
>
|
||||
<template #header>
|
||||
<NSpace justify="space-between" align="center">
|
||||
<NSpace align="center">
|
||||
<NText strong>{{ getParamMetadata(key)?.label || key }}</NText>
|
||||
<NTag v-if="!getParamMetadata(key)" type="info" size="small">
|
||||
{{ t('modelManager.advancedParameters.customParam') }}
|
||||
</NTag>
|
||||
</NSpace>
|
||||
<NButton
|
||||
@click="removeLLMParam(key)"
|
||||
size="tiny"
|
||||
type="error"
|
||||
quaternary
|
||||
circle
|
||||
>
|
||||
×
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<NSpace vertical :size="8">
|
||||
<NText v-if="getParamMetadata(key)?.description" depth="3" style="font-size: 12px;">
|
||||
{{ getParamMetadata(key)?.description }}
|
||||
</NText>
|
||||
|
||||
<NCheckbox
|
||||
v-if="getParamMetadata(key)?.type === 'boolean'"
|
||||
v-model:checked="currentParamOverrides[key]"
|
||||
>
|
||||
{{ currentParamOverrides[key] ? t('common.enabled') : t('common.disabled') }}
|
||||
</NCheckbox>
|
||||
|
||||
<NSpace
|
||||
v-else-if="getParamMetadata(key)?.type === 'number' || (getParamMetadata(key)?.type === 'integer' && getParamMetadata(key)?.name !== 'stopSequences')"
|
||||
vertical
|
||||
:size="4"
|
||||
>
|
||||
<NInputNumber
|
||||
v-model:value="currentParamOverrides[key] as number"
|
||||
:min="getParamMetadata(key)?.minValue"
|
||||
:max="getParamMetadata(key)?.maxValue"
|
||||
:step="getParamMetadata(key)?.step"
|
||||
:placeholder="getParamMetadata(key)?.defaultValue !== undefined ? String(getParamMetadata(key)?.defaultValue) : ''"
|
||||
:status="isParamInvalid(key, currentParamOverrides[key]) ? 'error' : undefined"
|
||||
/>
|
||||
<NText
|
||||
v-if="getParamMetadata(key)?.minValue !== undefined && getParamMetadata(key)?.maxValue !== undefined"
|
||||
depth="3"
|
||||
style="font-size: 12px;"
|
||||
>
|
||||
范围: {{ getParamMetadata(key)?.minValue }} - {{ getParamMetadata(key)?.maxValue }}{{ getParamMetadata(key)?.unit || '' }}
|
||||
</NText>
|
||||
<NText
|
||||
v-if="isParamInvalid(key, currentParamOverrides[key])"
|
||||
type="error"
|
||||
style="font-size: 12px;"
|
||||
>
|
||||
{{ getParamValidationMessage(key, currentParamOverrides[key]) }}
|
||||
</NText>
|
||||
</NSpace>
|
||||
|
||||
<NSpace v-else vertical :size="4">
|
||||
<NInput
|
||||
v-model:value="currentParamOverrides[key] as string"
|
||||
:placeholder="getParamMetadata(key)?.name === 'stopSequences' ? t('modelManager.advancedParameters.stopSequencesPlaceholder') : (getParamMetadata(key)?.defaultValue !== undefined ? String(getParamMetadata(key)?.defaultValue) : '')"
|
||||
/>
|
||||
<NText
|
||||
v-if="getParamMetadata(key)?.name === 'stopSequences'"
|
||||
depth="3"
|
||||
style="font-size: 12px;"
|
||||
>
|
||||
{{ t('modelManager.advancedParameters.stopSequencesPlaceholder') }}
|
||||
</NText>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</form>
|
||||
</NScrollbar>
|
||||
|
||||
<div v-else style="height: 200px; display: flex; align-items: center; justify-content: center;">
|
||||
<NSpin />
|
||||
</div>
|
||||
|
||||
<template #action>
|
||||
<NSpace justify="space-between" align="center" style="width: 100%;">
|
||||
<NSpace align="center">
|
||||
<NButton
|
||||
@click="testFormConnection"
|
||||
:loading="isTestingFormConnection"
|
||||
:disabled="!canTestFormConnection"
|
||||
secondary
|
||||
type="info"
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
||||
</svg>
|
||||
</template>
|
||||
{{ t('modelManager.testConnection') }}
|
||||
</NButton>
|
||||
<NTag v-if="formConnectionStatus" :type="formConnectionStatus.type" size="small" :bordered="false">
|
||||
{{ formConnectionStatus.message }}
|
||||
</NTag>
|
||||
</NSpace>
|
||||
|
||||
<NSpace>
|
||||
<NButton @click="handleCancel">{{ t('common.cancel') }}</NButton>
|
||||
<NButton type="primary" :loading="isSaving" @click="handleSubmit">
|
||||
{{ isEditing ? t('common.save') : t('common.create') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
NModal,
|
||||
NScrollbar,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NH4,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NSelect,
|
||||
NCheckbox,
|
||||
NSpace,
|
||||
NButton,
|
||||
NDivider,
|
||||
NText,
|
||||
NTag,
|
||||
NTooltip,
|
||||
NCard,
|
||||
NSpin
|
||||
} from 'naive-ui'
|
||||
import type { TextModelManager } from '../composables/useTextModelManager'
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
configId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:show', 'saved'])
|
||||
|
||||
const { t } = useI18n()
|
||||
const manager = inject<TextModelManager>('textModelManager')
|
||||
if (!manager) {
|
||||
throw new Error('Text model manager not provided')
|
||||
}
|
||||
|
||||
const modalTitle = manager.modalTitle
|
||||
const form = manager.form
|
||||
const formReady = manager.formReady
|
||||
const providerOptions = manager.providerOptions
|
||||
const isLoadingProviders = manager.isLoadingProviders
|
||||
const connectionFields = manager.connectionFields
|
||||
const modelOptions = manager.modelOptions
|
||||
const isLoadingModelOptions = manager.isLoadingModelOptions
|
||||
const availableLLMParamDefinitions = manager.availableLLMParamDefinitions
|
||||
const canRefreshModelOptions = manager.canRefreshModelOptions
|
||||
const refreshModelOptions = manager.refreshModelOptions
|
||||
const selectedNewLLMParamId = manager.selectedNewLLMParamId
|
||||
const handleQuickAddParam = manager.handleQuickAddParam
|
||||
const customLLMParam = manager.customLLMParam
|
||||
const cancelCustomParam = manager.cancelCustomParam
|
||||
const handleCustomParamAdd = manager.handleCustomParamAdd
|
||||
const currentParamOverrides = manager.currentParamOverrides
|
||||
const currentProviderType = manager.currentProviderType
|
||||
const getParamMetadata = manager.getParamMetadata
|
||||
const removeLLMParam = manager.removeLLMParam
|
||||
const isParamInvalid = manager.isParamInvalid
|
||||
const getParamValidationMessage = manager.getParamValidationMessage
|
||||
const formConnectionStatus = manager.formConnectionStatus
|
||||
const testFormConnection = manager.testFormConnection
|
||||
const isTestingFormConnection = manager.isTestingFormConnection
|
||||
const canTestFormConnection = manager.canTestFormConnection
|
||||
const isSaving = manager.isSaving
|
||||
|
||||
const isEditing = computed(() => !!manager.editingModelId.value)
|
||||
|
||||
const handleUpdateShow = (value: boolean) => {
|
||||
if (!value) {
|
||||
manager.resetFormState()
|
||||
}
|
||||
emit('update:show', value)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const id = await manager.saveForm()
|
||||
emit('saved', id || undefined)
|
||||
handleUpdateShow(false)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
handleUpdateShow(false)
|
||||
}
|
||||
|
||||
const onProviderChange = (providerId: string) => {
|
||||
manager.selectProvider(providerId, !isEditing.value)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<NSpace vertical :size="12">
|
||||
<NCard
|
||||
v-for="model in models"
|
||||
:key="model.id"
|
||||
hoverable
|
||||
:style="{ opacity: model.enabled ? 1 : 0.6 }"
|
||||
>
|
||||
<template #header>
|
||||
<NSpace justify="space-between" align="center">
|
||||
<NSpace vertical :size="2">
|
||||
<NSpace align="center">
|
||||
<NText strong>{{ model.name }}</NText>
|
||||
<NTag v-if="!model.enabled" type="warning" size="small">
|
||||
{{ t('modelManager.disabled') }}
|
||||
</NTag>
|
||||
</NSpace>
|
||||
|
||||
<NSpace :size="6">
|
||||
<NTag size="small" type="info" :bordered="false">
|
||||
{{ model.providerMeta?.name || model.providerMeta?.id }}
|
||||
</NTag>
|
||||
<NTag size="small" type="primary" :bordered="false">
|
||||
{{ model.modelMeta?.name || model.modelMeta?.id }}
|
||||
</NTag>
|
||||
<NTag
|
||||
v-if="model.modelMeta?.capabilities?.supportsTools"
|
||||
size="small"
|
||||
type="success"
|
||||
:bordered="false"
|
||||
>
|
||||
{{ t('modelManager.capabilities.tools') }}
|
||||
</NTag>
|
||||
<NTag
|
||||
v-if="model.modelMeta?.capabilities?.supportsReasoning"
|
||||
size="small"
|
||||
type="warning"
|
||||
:bordered="false"
|
||||
>
|
||||
{{ t('modelManager.capabilities.reasoning') }}
|
||||
</NTag>
|
||||
<NTag
|
||||
v-if="model.modelMeta?.capabilities?.supportsStreaming"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
>
|
||||
{{ t('modelManager.capabilities.streaming') }}
|
||||
</NTag>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<template #header-extra>
|
||||
<NSpace @click.stop>
|
||||
<NButton
|
||||
@click="emit('test', model.id)"
|
||||
size="small"
|
||||
quaternary
|
||||
:disabled="isTestingConnectionFor(model.id)"
|
||||
:loading="isTestingConnectionFor(model.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="h-4 w-4"
|
||||
>
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||
</svg>
|
||||
</template>
|
||||
<span class="hidden md:inline">{{ t('modelManager.testConnection') }}</span>
|
||||
</NButton>
|
||||
|
||||
<NButton @click="emit('edit', model.id)" size="small" quaternary>
|
||||
<template #icon>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="h-4 w-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<span class="hidden md:inline">{{ t('modelManager.editModel') }}</span>
|
||||
</NButton>
|
||||
|
||||
<NButton
|
||||
@click="emit(model.enabled ? 'disable' : 'enable', model.id)"
|
||||
size="small"
|
||||
:type="model.enabled ? 'warning' : 'success'"
|
||||
quaternary
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
v-if="model.enabled"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="h-4 w-4"
|
||||
>
|
||||
<path d="M12 6v.343" />
|
||||
<path d="M18.218 18.218A7 7 0 0 1 5 15V9a7 7 0 0 1 .782-3.218" />
|
||||
<path d="M19 13.343V9A7 7 0 0 0 8.56 2.902" />
|
||||
<path d="M22 22 2 2" />
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="h-4 w-4"
|
||||
>
|
||||
<rect x="5" y="2" width="14" height="20" rx="7" />
|
||||
<path d="M12 6v4" />
|
||||
</svg>
|
||||
</template>
|
||||
<span class="hidden md:inline">
|
||||
{{ model.enabled ? t('common.disable') : t('common.enable') }}
|
||||
</span>
|
||||
</NButton>
|
||||
|
||||
<NButton
|
||||
v-if="!isDefaultModel(model.id)"
|
||||
@click="emit('delete', model.id)"
|
||||
size="small"
|
||||
type="error"
|
||||
quaternary
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="h-4 w-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<span class="hidden md:inline">{{ t('common.delete') }}</span>
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { NButton, NCard, NTag, NText, NSpace } from 'naive-ui'
|
||||
import type { TextModelConfig } from '@prompt-optimizer/core'
|
||||
|
||||
const props = defineProps({
|
||||
models: {
|
||||
type: Array as PropType<TextModelConfig[]>,
|
||||
default: () => []
|
||||
},
|
||||
isTestingConnectionFor: {
|
||||
type: Function as PropType<(id: string) => boolean>,
|
||||
required: true
|
||||
},
|
||||
isDefaultModel: {
|
||||
type: Function as PropType<(id: string) => boolean>,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['test', 'edit', 'enable', 'disable', 'delete'])
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="text-model-manager">
|
||||
<TextModelList
|
||||
:models="manager.models.value"
|
||||
:is-testing-connection-for="isTestingConnectionFor"
|
||||
:is-default-model="manager.isDefaultModel"
|
||||
@test="handleTestConnection"
|
||||
@edit="handleEditModel"
|
||||
@enable="handleEnableModel"
|
||||
@disable="handleDisableModel"
|
||||
@delete="handleDeleteModel"
|
||||
/>
|
||||
|
||||
<TextModelEditModal
|
||||
:show="showEditModal"
|
||||
:config-id="editingModelId || ''"
|
||||
@update:show="updateEditModalVisibility"
|
||||
@saved="handleModelUpdated"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, provide, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useTextModelManager } from '../composables/useTextModelManager'
|
||||
import TextModelList from './TextModelList.vue'
|
||||
import TextModelEditModal from './TextModelEditModal.vue'
|
||||
|
||||
const emit = defineEmits(['modelsUpdated'])
|
||||
const { t } = useI18n()
|
||||
const manager = useTextModelManager()
|
||||
provide('textModelManager', manager)
|
||||
|
||||
const showEditModal = ref(false)
|
||||
const editingModelId = ref<string | null>(null)
|
||||
const isTestingConnectionFor = (id: string) => !!manager.testingConnections.value[id]
|
||||
const handleModelUpdated = async (id?: string) => {
|
||||
await manager.loadModels()
|
||||
const targetId = id || manager.models.value[0]?.id
|
||||
if (targetId) {
|
||||
emit('modelsUpdated', targetId)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTestConnection = async (id: string) => {
|
||||
await manager.testConfigConnection(id)
|
||||
}
|
||||
|
||||
const handleEditModel = async (id: string) => {
|
||||
await manager.prepareForEdit(id)
|
||||
editingModelId.value = id
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
const updateEditModalVisibility = (value: boolean) => {
|
||||
showEditModal.value = value
|
||||
if (!value) {
|
||||
editingModelId.value = null
|
||||
manager.resetFormState()
|
||||
}
|
||||
}
|
||||
|
||||
const handleEnableModel = async (id: string) => {
|
||||
await manager.enableModel(id)
|
||||
emit('modelsUpdated', id)
|
||||
}
|
||||
|
||||
const handleDisableModel = async (id: string) => {
|
||||
await manager.disableModel(id)
|
||||
emit('modelsUpdated', id)
|
||||
}
|
||||
|
||||
const handleDeleteModel = async (id: string) => {
|
||||
if (confirm(t('modelManager.deleteConfirm'))) {
|
||||
await manager.deleteModel(id)
|
||||
const firstId = manager.models.value[0]?.id
|
||||
if (firstId) {
|
||||
emit('modelsUpdated', firstId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const openAddModal = async () => {
|
||||
await manager.prepareForCreate()
|
||||
editingModelId.value = null
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openAddModal,
|
||||
refresh: manager.loadModels
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await manager.loadProviders()
|
||||
await manager.loadModels()
|
||||
const firstId = manager.models.value[0]?.id
|
||||
if (firstId) {
|
||||
emit('modelsUpdated', firstId)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-model-manager {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,748 @@
|
||||
import { ref, computed, inject, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useToast } from './useToast'
|
||||
import {
|
||||
advancedParameterDefinitions,
|
||||
type ModelOption,
|
||||
type TextModel,
|
||||
type TextModelConfig,
|
||||
type TextProvider
|
||||
} from '@prompt-optimizer/core'
|
||||
|
||||
type TextConnectionValue = string | number | boolean | undefined
|
||||
interface TextConnectionConfig {
|
||||
[key: string]: TextConnectionValue
|
||||
}
|
||||
|
||||
interface TextModelForm {
|
||||
id: string
|
||||
originalId?: string
|
||||
name: string
|
||||
enabled: boolean
|
||||
providerId: string
|
||||
modelId: string
|
||||
connectionConfig: TextConnectionConfig
|
||||
paramOverrides: Record<string, unknown>
|
||||
displayMaskedKey: boolean
|
||||
originalApiKey?: string
|
||||
defaultModel?: string
|
||||
}
|
||||
|
||||
const DEFAULT_TEXT_MODEL_IDS = ['openai', 'gemini', 'deepseek', 'zhipu', 'siliconflow', 'custom'] as const
|
||||
|
||||
export function useTextModelManager() {
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const services = inject<any>('services')
|
||||
if (!services) {
|
||||
throw new Error('Services not provided!')
|
||||
}
|
||||
|
||||
const modelManager = services.value.modelManager
|
||||
const llmService = services.value.llmService
|
||||
const textAdapterRegistry = services.value.textAdapterRegistry
|
||||
|
||||
const models = ref<TextModelConfig[]>([])
|
||||
const loadingModels = ref(false)
|
||||
const testingConnections = ref<Record<string, boolean>>({})
|
||||
|
||||
const providers = ref<TextProvider[]>([])
|
||||
const providersLoaded = ref(false)
|
||||
const isLoadingProviders = ref(false)
|
||||
|
||||
const form = ref<TextModelForm>({
|
||||
id: '',
|
||||
name: '',
|
||||
enabled: true,
|
||||
providerId: '',
|
||||
modelId: '',
|
||||
connectionConfig: {},
|
||||
paramOverrides: {},
|
||||
displayMaskedKey: false
|
||||
})
|
||||
|
||||
const editingModelId = ref<string | null>(null)
|
||||
const formReady = ref(false)
|
||||
const isSaving = ref(false)
|
||||
const isTestingFormConnection = ref(false)
|
||||
const formConnectionStatus = ref<{
|
||||
type: 'success' | 'error' | 'warning' | 'info'
|
||||
message: string
|
||||
} | null>(null)
|
||||
|
||||
const modelOptions = ref<ModelOption[]>([])
|
||||
const isLoadingModelOptions = ref(false)
|
||||
const selectedNewLLMParamId = ref('')
|
||||
const customLLMParam = ref({ key: '', value: '' })
|
||||
|
||||
const currentParamOverrides = computed(() => form.value.paramOverrides || {})
|
||||
const currentProviderType = computed(() => form.value.providerId || 'custom')
|
||||
|
||||
const providerOptions = computed(() =>
|
||||
providers.value.map(provider => ({
|
||||
label: provider.name,
|
||||
value: provider.id,
|
||||
disabled: false
|
||||
}))
|
||||
)
|
||||
|
||||
const selectedProvider = computed(() => {
|
||||
if (!form.value.providerId) return null
|
||||
return providers.value.find(p => p.id === form.value.providerId) || null
|
||||
})
|
||||
|
||||
const connectionFields = computed(() => {
|
||||
if (!selectedProvider.value?.connectionSchema) return []
|
||||
|
||||
const schema = selectedProvider.value.connectionSchema
|
||||
const fields: Array<{ name: string; required: boolean; type: string; placeholder?: string }> = []
|
||||
|
||||
for (const fieldName of schema.required) {
|
||||
fields.push({
|
||||
name: fieldName,
|
||||
required: true,
|
||||
type: schema.fieldTypes[fieldName] || 'string',
|
||||
placeholder: fieldName === 'baseURL' ? selectedProvider.value.defaultBaseURL : ''
|
||||
})
|
||||
}
|
||||
|
||||
for (const fieldName of schema.optional) {
|
||||
fields.push({
|
||||
name: fieldName,
|
||||
required: false,
|
||||
type: schema.fieldTypes[fieldName] || 'string',
|
||||
placeholder: fieldName === 'baseURL' ? selectedProvider.value.defaultBaseURL : ''
|
||||
})
|
||||
}
|
||||
|
||||
return fields
|
||||
})
|
||||
|
||||
const availableLLMParamDefinitions = computed(() => {
|
||||
if (!advancedParameterDefinitions) return []
|
||||
const currentParams = currentParamOverrides.value || {}
|
||||
const provider = currentProviderType.value || 'custom'
|
||||
|
||||
return advancedParameterDefinitions.filter(def =>
|
||||
def.appliesToProviders.includes(provider) &&
|
||||
!Object.prototype.hasOwnProperty.call(currentParams, def.name)
|
||||
)
|
||||
})
|
||||
|
||||
const canTestFormConnection = computed(() => !!editingModelId.value && !isTestingFormConnection.value)
|
||||
const canRefreshModelOptions = computed(() => !!selectedProvider.value && !isLoadingModelOptions.value)
|
||||
|
||||
const modalTitle = computed(() => (editingModelId.value ? t('modelManager.editModel') : t('modelManager.addModel')))
|
||||
|
||||
const isDefaultModel = (id: string) => {
|
||||
return DEFAULT_TEXT_MODEL_IDS.includes(id as typeof DEFAULT_TEXT_MODEL_IDS[number])
|
||||
}
|
||||
|
||||
const resetFormState = () => {
|
||||
form.value = {
|
||||
id: '',
|
||||
name: '',
|
||||
enabled: true,
|
||||
providerId: '',
|
||||
modelId: '',
|
||||
connectionConfig: {},
|
||||
paramOverrides: {},
|
||||
displayMaskedKey: false
|
||||
}
|
||||
editingModelId.value = null
|
||||
formReady.value = false
|
||||
modelOptions.value = []
|
||||
selectedNewLLMParamId.value = ''
|
||||
customLLMParam.value = { key: '', value: '' }
|
||||
formConnectionStatus.value = null
|
||||
}
|
||||
|
||||
const ensureProvidersLoaded = async () => {
|
||||
if (providersLoaded.value) return
|
||||
isLoadingProviders.value = true
|
||||
try {
|
||||
providers.value = textAdapterRegistry?.getAllProviders?.() || []
|
||||
providersLoaded.value = true
|
||||
} catch (error) {
|
||||
console.error('加载文本模型提供商失败:', error)
|
||||
toast.error(t('modelManager.loadFailed'))
|
||||
providers.value = []
|
||||
} finally {
|
||||
isLoadingProviders.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadStaticModelsForProvider = (providerId: string) => {
|
||||
if (!providerId || !textAdapterRegistry) {
|
||||
modelOptions.value = []
|
||||
return
|
||||
}
|
||||
try {
|
||||
const staticModels = textAdapterRegistry.getStaticModels(providerId)
|
||||
modelOptions.value = staticModels.map(model => ({
|
||||
value: model.id,
|
||||
label: model.name || model.id
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('加载 Provider 模型失败:', error)
|
||||
modelOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const loadModels = async () => {
|
||||
loadingModels.value = true
|
||||
try {
|
||||
const all = await modelManager.getAllModels()
|
||||
models.value = all
|
||||
.map(model => ({ ...model }))
|
||||
.sort((a, b) => {
|
||||
if (a.enabled !== b.enabled) {
|
||||
return a.enabled ? -1 : 1
|
||||
}
|
||||
const aDefault = isDefaultModel(a.id)
|
||||
const bDefault = isDefaultModel(b.id)
|
||||
if (aDefault !== bDefault) {
|
||||
return aDefault ? -1 : 1
|
||||
}
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('加载模型失败:', error)
|
||||
toast.error(t('modelManager.loadFailed'))
|
||||
} finally {
|
||||
loadingModels.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const testConfigConnection = async (id: string) => {
|
||||
if (!id || testingConnections.value[id]) return
|
||||
testingConnections.value[id] = true
|
||||
try {
|
||||
const model = await modelManager.getModel(id)
|
||||
if (!model) {
|
||||
throw new Error(t('modelManager.noModelsAvailable'))
|
||||
}
|
||||
await llmService.testConnection(id)
|
||||
toast.success(t('modelManager.testSuccess', { provider: model.name }))
|
||||
} catch (error: any) {
|
||||
console.error('连接测试失败:', error)
|
||||
const model = await modelManager.getModel(id)
|
||||
const modelName = model?.name || id
|
||||
toast.error(t('modelManager.testFailed', {
|
||||
provider: modelName,
|
||||
error: error?.message || 'Unknown error'
|
||||
}))
|
||||
} finally {
|
||||
delete testingConnections.value[id]
|
||||
}
|
||||
}
|
||||
|
||||
const enableModel = async (id: string) => {
|
||||
try {
|
||||
const model = await modelManager.getModel(id)
|
||||
if (!model) throw new Error(t('modelManager.noModelsAvailable'))
|
||||
await modelManager.enableModel(id)
|
||||
await loadModels()
|
||||
toast.success(t('modelManager.enableSuccess'))
|
||||
} catch (error: any) {
|
||||
console.error('启用模型失败:', error)
|
||||
toast.error(t('modelManager.enableFailed', { error: error.message }))
|
||||
}
|
||||
}
|
||||
|
||||
const disableModel = async (id: string) => {
|
||||
try {
|
||||
const model = await modelManager.getModel(id)
|
||||
if (!model) throw new Error(t('modelManager.noModelsAvailable'))
|
||||
await modelManager.disableModel(id)
|
||||
await loadModels()
|
||||
toast.success(t('modelManager.disableSuccess'))
|
||||
} catch (error: any) {
|
||||
console.error('禁用模型失败:', error)
|
||||
toast.error(t('modelManager.disableFailed', { error: error.message }))
|
||||
}
|
||||
}
|
||||
|
||||
const deleteModel = async (id: string) => {
|
||||
try {
|
||||
await modelManager.deleteModel(id)
|
||||
await loadModels()
|
||||
toast.success(t('modelManager.deleteSuccess'))
|
||||
} catch (error: any) {
|
||||
console.error('删除模型失败:', error)
|
||||
toast.error(t('modelManager.deleteFailed', { error: error.message }))
|
||||
}
|
||||
}
|
||||
|
||||
const maskApiKey = (value: string) => {
|
||||
const keyLength = value.length
|
||||
if (keyLength <= 8) return '*'.repeat(keyLength)
|
||||
const visiblePart = 4
|
||||
const prefix = value.substring(0, visiblePart)
|
||||
const suffix = value.substring(keyLength - visiblePart)
|
||||
const maskedLength = keyLength - visiblePart * 2
|
||||
return `${prefix}${'*'.repeat(maskedLength)}${suffix}`
|
||||
}
|
||||
|
||||
const setProvider = (providerId: string, autoSelectFirstModel = true) => {
|
||||
form.value.providerId = providerId
|
||||
formConnectionStatus.value = null
|
||||
|
||||
if (!providerId) {
|
||||
form.value.connectionConfig = {}
|
||||
modelOptions.value = []
|
||||
return
|
||||
}
|
||||
|
||||
loadStaticModelsForProvider(providerId)
|
||||
|
||||
const providerMeta = providers.value.find(p => p.id === providerId)
|
||||
if (providerMeta?.defaultBaseURL) {
|
||||
form.value.connectionConfig = {
|
||||
...form.value.connectionConfig,
|
||||
baseURL: form.value.connectionConfig.baseURL || providerMeta.defaultBaseURL
|
||||
}
|
||||
}
|
||||
|
||||
if (autoSelectFirstModel && modelOptions.value.length > 0) {
|
||||
form.value.modelId = modelOptions.value[0].value
|
||||
form.value.defaultModel = modelOptions.value[0].value
|
||||
}
|
||||
}
|
||||
|
||||
const prepareForCreate = async () => {
|
||||
resetFormState()
|
||||
await ensureProvidersLoaded()
|
||||
formReady.value = false
|
||||
|
||||
if (providers.value.length > 0) {
|
||||
setProvider(providers.value[0].id, true)
|
||||
}
|
||||
|
||||
formReady.value = true
|
||||
}
|
||||
|
||||
const prepareForEdit = async (id: string) => {
|
||||
resetFormState()
|
||||
editingModelId.value = id
|
||||
await ensureProvidersLoaded()
|
||||
formReady.value = false
|
||||
|
||||
try {
|
||||
const model = await modelManager.getModel(id)
|
||||
if (!model) {
|
||||
throw new Error(t('modelManager.noModelsAvailable'))
|
||||
}
|
||||
|
||||
const connectionConfig: TextConnectionConfig = { ...(model.connectionConfig ?? {}) }
|
||||
const rawApiKey = connectionConfig.apiKey ?? ''
|
||||
if (rawApiKey) {
|
||||
connectionConfig.apiKey = maskApiKey(rawApiKey)
|
||||
}
|
||||
|
||||
form.value = {
|
||||
id: model.id,
|
||||
originalId: model.id,
|
||||
name: model.name,
|
||||
enabled: model.enabled,
|
||||
providerId: model.providerMeta?.id ?? 'custom',
|
||||
modelId: model.modelMeta?.id ?? '',
|
||||
connectionConfig,
|
||||
paramOverrides: model.paramOverrides ? JSON.parse(JSON.stringify(model.paramOverrides)) : {},
|
||||
displayMaskedKey: !!rawApiKey,
|
||||
originalApiKey: rawApiKey || undefined,
|
||||
defaultModel: model.modelMeta?.id ?? ''
|
||||
}
|
||||
|
||||
setProvider(form.value.providerId, false)
|
||||
if (!modelOptions.value.some(option => option.value === form.value.modelId) && form.value.modelId) {
|
||||
modelOptions.value.push({ value: form.value.modelId, label: form.value.modelId })
|
||||
}
|
||||
|
||||
await refreshModelOptions(false)
|
||||
} catch (error) {
|
||||
console.error('加载模型失败:', error)
|
||||
toast.error(t('modelManager.loadFailed'))
|
||||
} finally {
|
||||
formReady.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const refreshModelOptions = async (showSuccess = true) => {
|
||||
if (!form.value.providerId) return
|
||||
|
||||
const baseURL = form.value.connectionConfig.baseURL?.trim()
|
||||
if (!baseURL) {
|
||||
toast.error(t('modelManager.needBaseUrl'))
|
||||
return
|
||||
}
|
||||
|
||||
isLoadingModelOptions.value = true
|
||||
|
||||
try {
|
||||
const providerTemplateId = form.value.providerId || currentProviderType.value || 'custom'
|
||||
const connectionConfig: TextConnectionConfig = {
|
||||
...form.value.connectionConfig,
|
||||
baseURL,
|
||||
apiKey: form.value.displayMaskedKey && form.value.originalApiKey
|
||||
? form.value.originalApiKey
|
||||
: form.value.connectionConfig.apiKey
|
||||
}
|
||||
|
||||
const existingConfig = form.value.originalId ? await modelManager.getModel(form.value.originalId) : undefined
|
||||
|
||||
let providerMeta = providers.value.find(p => p.id === providerTemplateId) || existingConfig?.providerMeta
|
||||
let modelMeta = existingConfig?.modelMeta
|
||||
|
||||
if (textAdapterRegistry && providerTemplateId) {
|
||||
try {
|
||||
const adapter = textAdapterRegistry.getAdapter(providerTemplateId)
|
||||
if (!providerMeta) {
|
||||
providerMeta = adapter.getProvider()
|
||||
}
|
||||
const staticModels = adapter.getModels()
|
||||
if (form.value.modelId) {
|
||||
modelMeta = staticModels.find(m => m.id === form.value.modelId) || modelMeta
|
||||
}
|
||||
if (!modelMeta) {
|
||||
modelMeta = staticModels[0]
|
||||
}
|
||||
if (!modelMeta && form.value.modelId) {
|
||||
modelMeta = adapter.buildDefaultModel(form.value.modelId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`[useTextModelManager] Failed to load metadata for provider ${providerTemplateId}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchedModels = await llmService.fetchModelList(providerTemplateId, {
|
||||
connectionConfig,
|
||||
providerMeta,
|
||||
modelMeta: modelMeta ? { ...modelMeta, id: form.value.modelId || modelMeta.id } : undefined
|
||||
} as Partial<TextModelConfig>)
|
||||
|
||||
modelOptions.value = fetchedModels
|
||||
if (showSuccess) {
|
||||
toast.success(t('modelManager.fetchModelsSuccess', { count: fetchedModels.length }))
|
||||
}
|
||||
|
||||
if (fetchedModels.length > 0 && !fetchedModels.some(m => m.value === form.value.modelId)) {
|
||||
form.value.modelId = fetchedModels[0].value
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取模型列表失败:', error)
|
||||
toast.error(error?.message || t('modelManager.loadFailed'))
|
||||
modelOptions.value = []
|
||||
} finally {
|
||||
isLoadingModelOptions.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const ensureProviderMeta = (providerId: string, existing?: TextProvider) => {
|
||||
if (existing) return existing
|
||||
const adapter = textAdapterRegistry.getAdapter(providerId)
|
||||
return adapter.getProvider()
|
||||
}
|
||||
|
||||
const ensureModelMeta = (providerId: string, modelId: string, existing?: TextModel) => {
|
||||
const adapter = textAdapterRegistry.getAdapter(providerId)
|
||||
const staticModels = adapter.getModels()
|
||||
const foundModel = staticModels.find(m => m.id === modelId)
|
||||
if (foundModel) {
|
||||
return foundModel
|
||||
}
|
||||
return adapter.buildDefaultModel(modelId)
|
||||
}
|
||||
|
||||
const updateExistingModel = async () => {
|
||||
if (!form.value.originalId) {
|
||||
throw new Error('编辑会话无效')
|
||||
}
|
||||
|
||||
const existingConfig = await modelManager.getModel(form.value.originalId)
|
||||
if (!existingConfig) {
|
||||
throw new Error('模型不存在')
|
||||
}
|
||||
|
||||
const connectionConfig: TextConnectionConfig = {
|
||||
...form.value.connectionConfig,
|
||||
baseURL: form.value.connectionConfig.baseURL?.trim() || existingConfig.connectionConfig?.baseURL
|
||||
}
|
||||
|
||||
if (form.value.displayMaskedKey) {
|
||||
if (form.value.originalApiKey) {
|
||||
connectionConfig.apiKey = form.value.originalApiKey
|
||||
} else {
|
||||
delete connectionConfig.apiKey
|
||||
}
|
||||
} else if (form.value.connectionConfig.apiKey) {
|
||||
connectionConfig.apiKey = form.value.connectionConfig.apiKey
|
||||
} else {
|
||||
delete connectionConfig.apiKey
|
||||
}
|
||||
|
||||
const providerMeta = ensureProviderMeta(form.value.providerId, existingConfig.providerMeta)
|
||||
const modelMeta = ensureModelMeta(form.value.providerId, form.value.modelId, existingConfig.modelMeta)
|
||||
|
||||
const updates: Partial<TextModelConfig> = {
|
||||
name: form.value.name,
|
||||
enabled: form.value.enabled,
|
||||
providerMeta,
|
||||
modelMeta,
|
||||
connectionConfig,
|
||||
paramOverrides: { ...(form.value.paramOverrides || {}) }
|
||||
}
|
||||
|
||||
await modelManager.updateModel(form.value.originalId, updates)
|
||||
return form.value.originalId
|
||||
}
|
||||
|
||||
const createNewModel = async () => {
|
||||
if (!form.value.id) {
|
||||
toast.error(t('modelManager.modelKeyRequired'))
|
||||
throw new Error('模型标识必填')
|
||||
}
|
||||
|
||||
const providerMeta = ensureProviderMeta(form.value.providerId)
|
||||
const modelMeta = ensureModelMeta(form.value.providerId, form.value.defaultModel || form.value.modelId)
|
||||
|
||||
const newConfig: TextModelConfig = {
|
||||
id: form.value.id,
|
||||
name: form.value.name,
|
||||
enabled: true,
|
||||
providerMeta,
|
||||
modelMeta,
|
||||
connectionConfig: { ...form.value.connectionConfig },
|
||||
paramOverrides: { ...(form.value.paramOverrides ?? {}) }
|
||||
}
|
||||
|
||||
await modelManager.addModel(form.value.id, newConfig)
|
||||
return form.value.id
|
||||
}
|
||||
|
||||
const saveForm = async () => {
|
||||
if (isSaving.value) return null
|
||||
isSaving.value = true
|
||||
try {
|
||||
const savedId = editingModelId.value ? await updateExistingModel() : await createNewModel()
|
||||
await loadModels()
|
||||
return savedId
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const testFormConnection = async () => {
|
||||
if (!editingModelId.value || isTestingFormConnection.value) return
|
||||
isTestingFormConnection.value = true
|
||||
formConnectionStatus.value = { type: 'info', message: t('modelManager.testing') }
|
||||
try {
|
||||
await llmService.testConnection(editingModelId.value)
|
||||
formConnectionStatus.value = { type: 'success', message: t('modelManager.testSuccess', { provider: form.value.name }) }
|
||||
toast.success(t('modelManager.testSuccess', { provider: form.value.name }))
|
||||
} catch (error: any) {
|
||||
console.error('连接测试失败:', error)
|
||||
formConnectionStatus.value = {
|
||||
type: 'error',
|
||||
message: t('modelManager.testFailed', { provider: form.value.name, error: error?.message || 'Unknown error' })
|
||||
}
|
||||
toast.error(t('modelManager.testFailed', { provider: form.value.name, error: error?.message || 'Unknown error' }))
|
||||
} finally {
|
||||
isTestingFormConnection.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuickAddParam = () => {
|
||||
if (selectedNewLLMParamId.value !== 'custom') {
|
||||
quickAddLLMParam()
|
||||
}
|
||||
}
|
||||
|
||||
const quickAddLLMParam = () => {
|
||||
const paramsObject = currentParamOverrides.value
|
||||
if (!paramsObject) return
|
||||
if (selectedNewLLMParamId.value === 'custom') {
|
||||
if (customLLMParam.value.key && !Object.prototype.hasOwnProperty.call(paramsObject, customLLMParam.value.key)) {
|
||||
paramsObject[customLLMParam.value.key] = customLLMParam.value.value
|
||||
}
|
||||
customLLMParam.value = { key: '', value: '' }
|
||||
} else {
|
||||
const definition = advancedParameterDefinitions.find(def => def.id === selectedNewLLMParamId.value)
|
||||
if (definition && !Object.prototype.hasOwnProperty.call(paramsObject, definition.name)) {
|
||||
let val = definition.defaultValue
|
||||
if (definition.type === 'boolean') {
|
||||
val = val === undefined ? false : Boolean(val)
|
||||
} else if (definition.type === 'integer' && val !== undefined) {
|
||||
val = parseInt(String(val), 10)
|
||||
} else if (definition.type === 'number' && val !== undefined) {
|
||||
val = parseFloat(String(val))
|
||||
} else if (definition.name === 'stopSequences') {
|
||||
val = Array.isArray(val)
|
||||
? val
|
||||
: typeof val === 'string' && val ? val.split(',').map(s => s.trim()).filter(Boolean) : []
|
||||
}
|
||||
paramsObject[definition.name] = val
|
||||
}
|
||||
}
|
||||
selectedNewLLMParamId.value = ''
|
||||
}
|
||||
|
||||
const handleCustomParamAdd = () => {
|
||||
if (customLLMParam.value.key && customLLMParam.value.value) {
|
||||
quickAddLLMParam()
|
||||
}
|
||||
}
|
||||
|
||||
const cancelCustomParam = () => {
|
||||
selectedNewLLMParamId.value = ''
|
||||
customLLMParam.value = { key: '', value: '' }
|
||||
}
|
||||
|
||||
const removeLLMParam = (paramKey: string) => {
|
||||
if (!form.value.paramOverrides) return
|
||||
const overrides = { ...(form.value.paramOverrides as Record<string, unknown>) }
|
||||
delete overrides[paramKey]
|
||||
form.value.paramOverrides = overrides
|
||||
}
|
||||
|
||||
const getParamMetadata = (paramName: string) => {
|
||||
if (!advancedParameterDefinitions) return null
|
||||
const provider = currentProviderType.value || 'custom'
|
||||
const definition = advancedParameterDefinitions.find(
|
||||
def => def.name === paramName && def.appliesToProviders.includes(provider)
|
||||
)
|
||||
if (definition) {
|
||||
return {
|
||||
...definition,
|
||||
label: definition.labelKey ? t(definition.labelKey) : definition.name,
|
||||
description: definition.descriptionKey ? t(definition.descriptionKey) : `(${paramName})`,
|
||||
unit: definition.unitKey ? t(definition.unitKey) : definition.unit || ''
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const isParamInvalid = (paramName: string, value: unknown) => {
|
||||
const metadata = getParamMetadata(paramName)
|
||||
if (!metadata) {
|
||||
if (value === undefined || value === null || value === '') return false
|
||||
const dangerousNames = ['eval', 'exec', 'script', '__proto__', 'constructor']
|
||||
if (dangerousNames.some(dangerous => paramName.toLowerCase().includes(dangerous))) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (value === undefined || value === null || value === '') return false
|
||||
if (metadata.type === 'number' || metadata.type === 'integer') {
|
||||
const numValue = Number(value)
|
||||
if (Number.isNaN(numValue)) return true
|
||||
if (metadata.minValue !== undefined && numValue < metadata.minValue) return true
|
||||
if (metadata.maxValue !== undefined && numValue > metadata.maxValue) return true
|
||||
if (metadata.type === 'integer' && !Number.isInteger(numValue)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const getParamValidationMessage = (paramName: string, value: unknown) => {
|
||||
const metadata = getParamMetadata(paramName)
|
||||
if (!metadata) {
|
||||
const dangerousNames = ['eval', 'exec', 'script', '__proto__', 'constructor']
|
||||
if (dangerousNames.some(dangerous => paramName.toLowerCase().includes(dangerous))) {
|
||||
return t('modelManager.advancedParameters.validation.dangerousParam')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
if (metadata.type === 'number' || metadata.type === 'integer') {
|
||||
const numValue = Number(value)
|
||||
if (Number.isNaN(numValue)) {
|
||||
return t('modelManager.advancedParameters.validation.invalidNumber', {
|
||||
type: metadata.type === 'integer' ? t('common.integer') : t('common.number')
|
||||
})
|
||||
}
|
||||
if (metadata.minValue !== undefined && numValue < metadata.minValue) {
|
||||
return t('modelManager.advancedParameters.validation.belowMin', {
|
||||
min: metadata.minValue
|
||||
})
|
||||
}
|
||||
if (metadata.maxValue !== undefined && numValue > metadata.maxValue) {
|
||||
return t('modelManager.advancedParameters.validation.aboveMax', {
|
||||
max: metadata.maxValue
|
||||
})
|
||||
}
|
||||
if (metadata.type === 'integer' && !Number.isInteger(numValue)) {
|
||||
return t('modelManager.advancedParameters.validation.mustBeInteger')
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
watch(() => form.value.connectionConfig.apiKey, (newValue) => {
|
||||
if (!editingModelId.value) return
|
||||
const val = newValue ?? ''
|
||||
const isMasked = typeof val === 'string' && val.includes('*')
|
||||
form.value.displayMaskedKey = isMasked
|
||||
if (!isMasked && typeof val === 'string') {
|
||||
form.value.originalApiKey = val
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => form.value.modelId, (val) => {
|
||||
form.value.defaultModel = val || ''
|
||||
})
|
||||
|
||||
return {
|
||||
// list state
|
||||
models,
|
||||
loadingModels,
|
||||
loadModels,
|
||||
isDefaultModel,
|
||||
testingConnections,
|
||||
testConfigConnection,
|
||||
enableModel,
|
||||
disableModel,
|
||||
deleteModel,
|
||||
|
||||
// providers
|
||||
providers,
|
||||
isLoadingProviders,
|
||||
loadProviders: ensureProvidersLoaded,
|
||||
selectProvider: setProvider,
|
||||
|
||||
// form state & helpers
|
||||
form,
|
||||
formReady,
|
||||
isSaving,
|
||||
modalTitle,
|
||||
editingModelId,
|
||||
providerOptions,
|
||||
connectionFields,
|
||||
modelOptions,
|
||||
isLoadingModelOptions,
|
||||
availableLLMParamDefinitions,
|
||||
selectedNewLLMParamId,
|
||||
customLLMParam,
|
||||
currentParamOverrides,
|
||||
currentProviderType,
|
||||
selectedProvider,
|
||||
prepareForCreate,
|
||||
prepareForEdit,
|
||||
refreshModelOptions,
|
||||
saveForm,
|
||||
resetFormState,
|
||||
isTestingFormConnection,
|
||||
canTestFormConnection,
|
||||
testFormConnection,
|
||||
canRefreshModelOptions,
|
||||
handleQuickAddParam,
|
||||
handleCustomParamAdd,
|
||||
cancelCustomParam,
|
||||
removeLLMParam,
|
||||
getParamMetadata,
|
||||
isParamInvalid,
|
||||
getParamValidationMessage,
|
||||
formConnectionStatus
|
||||
}
|
||||
}
|
||||
|
||||
export type TextModelManager = ReturnType<typeof useTextModelManager>
|
||||
@@ -429,6 +429,7 @@ export default {
|
||||
deleteConfirm: 'Are you sure you want to delete this model? This action cannot be undone.',
|
||||
|
||||
// Operation Results
|
||||
testing: 'Testing connection...',
|
||||
testSuccess: 'Connection successful for {provider}!',
|
||||
testFailed: 'Connection failed for {provider}: {error}',
|
||||
updateSuccess: 'Update successful',
|
||||
|
||||
@@ -429,6 +429,7 @@
|
||||
deleteConfirm: '确定要删除此模型吗?此操作不可恢复。',
|
||||
|
||||
// 操作结果
|
||||
testing: '正在测试连接...',
|
||||
testSuccess: '{provider}连接测试成功',
|
||||
testFailed: '{provider}连接测试失败:{error}',
|
||||
updateSuccess: '更新成功',
|
||||
|
||||
@@ -429,6 +429,7 @@ export default {
|
||||
deleteConfirm: '確定要刪除此模型嗎?此操作無法復原。',
|
||||
|
||||
// 操作結果
|
||||
testing: '正在測試連線...',
|
||||
testSuccess: '{provider}連線測試成功',
|
||||
testFailed: '{provider}連線測試失敗:{error}',
|
||||
updateSuccess: '更新成功',
|
||||
|
||||
Vendored
+1
@@ -85,6 +85,7 @@ declare module 'vue-i18n' {
|
||||
deleteConfirm: string;
|
||||
|
||||
// Operation Results
|
||||
testing: string;
|
||||
testSuccess: string;
|
||||
testFailed: string;
|
||||
updateSuccess: string;
|
||||
|
||||
Reference in new issue
Block a user