diff --git a/packages/ui/src/components/ImageModelEditModal.vue b/packages/ui/src/components/ImageModelEditModal.vue index 127b31ea..81f61571 100644 --- a/packages/ui/src/components/ImageModelEditModal.vue +++ b/packages/ui/src/components/ImageModelEditModal.vue @@ -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" /> diff --git a/packages/ui/src/components/ModelManager.vue b/packages/ui/src/components/ModelManager.vue index c9b1d17d..d099f02f 100644 --- a/packages/ui/src/components/ModelManager.vue +++ b/packages/ui/src/components/ModelManager.vue @@ -17,7 +17,21 @@ ghost > {{ t('modelManager.addModel') }} @@ -28,688 +42,42 @@ ghost > {{ t('modelManager.addImageModel') }} - + - - + + + + + + - - - - - - - - -
- - - - - - - - - 提供商配置 - - - - - - - - - - - - - - - - - - 模型配置 - - - - - - - - - - - {{ t('modelManager.advancedParameters.title') }} - - {{ t('modelManager.advancedParameters.currentProvider') }}: - {{ currentProviderType === 'custom' ? t('modelManager.advancedParameters.customProvider') : currentProviderType.toUpperCase() }} - ({{ availableLLMParamDefinitions.length }}{{ t('modelManager.advancedParameters.availableParams') }}) - ({{ t('modelManager.advancedParameters.noAvailableParams') }}) - - - - - - - - - - - - 参数名称 - - - - 参数值 - - - - {{ t('common.cancel') }} - - {{ t('common.add') }} - - - - - - - {{ t('modelManager.advancedParameters.noParamsConfigured') }} - - - - - - - - - - {{ getParamMetadata(key)?.description }} - - - - - {{ currentParamOverrides[key] ? t('common.enabled') : t('common.disabled') }} - - - - - - - - 范围: {{ getParamMetadata(key)?.minValue }} - {{ getParamMetadata(key)?.maxValue }}{{ getParamMetadata(key)?.unit || '' }} - - - - {{ getParamValidationMessage(key, currentParamOverrides[key]) }} - - - - - - - - {{ t('modelManager.advancedParameters.stopSequencesPlaceholder') }} - - - - - - -
- - -
- - - - -
- - - - - - - - - - - - - 提供商配置 - - - - - - - - - - - - - - - - - - 模型配置 - - - - - - - - - - - {{ t('modelManager.advancedParameters.title') }} - - {{ t('modelManager.advancedParameters.currentProvider') }}: - {{ currentProviderType === 'custom' ? t('modelManager.advancedParameters.customProvider') : currentProviderType.toUpperCase() }} - ({{ availableLLMParamDefinitions.length }}{{ t('modelManager.advancedParameters.availableParams') }}) - ({{ t('modelManager.advancedParameters.noAvailableParams') }}) - - - - - - - - - - - - 参数名称 - - - - 参数值 - - - - {{ t('common.cancel') }} - - {{ t('common.add') }} - - - - - - - {{ t('modelManager.advancedParameters.noParamsConfigured') }} - - - - - - - - - - {{ getParamMetadata(key)?.description }} - - - - - {{ currentParamOverrides[key] ? t('common.enabled') : t('common.disabled') }} - - - - - - - - 范围: {{ getParamMetadata(key)?.minValue }} - {{ getParamMetadata(key)?.maxValue }}{{ getParamMetadata(key)?.unit || '' }} - - - - {{ getParamValidationMessage(key, currentParamOverrides[key]) }} - - - - - - - - {{ t('modelManager.advancedParameters.stopSequencesPlaceholder') }} - - - - - - -
- - -
- - diff --git a/packages/ui/src/components/TextModelEditModal.vue b/packages/ui/src/components/TextModelEditModal.vue new file mode 100644 index 00000000..29dc2016 --- /dev/null +++ b/packages/ui/src/components/TextModelEditModal.vue @@ -0,0 +1,422 @@ + + + diff --git a/packages/ui/src/components/TextModelList.vue b/packages/ui/src/components/TextModelList.vue new file mode 100644 index 00000000..13bc1a5c --- /dev/null +++ b/packages/ui/src/components/TextModelList.vue @@ -0,0 +1,201 @@ + + + diff --git a/packages/ui/src/components/TextModelManager.vue b/packages/ui/src/components/TextModelManager.vue new file mode 100644 index 00000000..8758377f --- /dev/null +++ b/packages/ui/src/components/TextModelManager.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/packages/ui/src/composables/useTextModelManager.ts b/packages/ui/src/composables/useTextModelManager.ts new file mode 100644 index 00000000..d878faf7 --- /dev/null +++ b/packages/ui/src/composables/useTextModelManager.ts @@ -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 + 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('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([]) + const loadingModels = ref(false) + const testingConnections = ref>({}) + + const providers = ref([]) + const providersLoaded = ref(false) + const isLoadingProviders = ref(false) + + const form = ref({ + id: '', + name: '', + enabled: true, + providerId: '', + modelId: '', + connectionConfig: {}, + paramOverrides: {}, + displayMaskedKey: false + }) + + const editingModelId = ref(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([]) + 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) + + 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 = { + 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) } + 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 diff --git a/packages/ui/src/i18n/locales/en-US.ts b/packages/ui/src/i18n/locales/en-US.ts index a02aef83..39962be2 100644 --- a/packages/ui/src/i18n/locales/en-US.ts +++ b/packages/ui/src/i18n/locales/en-US.ts @@ -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', diff --git a/packages/ui/src/i18n/locales/zh-CN.ts b/packages/ui/src/i18n/locales/zh-CN.ts index 2b65bcf4..038d4b96 100644 --- a/packages/ui/src/i18n/locales/zh-CN.ts +++ b/packages/ui/src/i18n/locales/zh-CN.ts @@ -429,6 +429,7 @@ deleteConfirm: '确定要删除此模型吗?此操作不可恢复。', // 操作结果 + testing: '正在测试连接...', testSuccess: '{provider}连接测试成功', testFailed: '{provider}连接测试失败:{error}', updateSuccess: '更新成功', diff --git a/packages/ui/src/i18n/locales/zh-TW.ts b/packages/ui/src/i18n/locales/zh-TW.ts index ec894d8c..51c150d4 100644 --- a/packages/ui/src/i18n/locales/zh-TW.ts +++ b/packages/ui/src/i18n/locales/zh-TW.ts @@ -429,6 +429,7 @@ export default { deleteConfirm: '確定要刪除此模型嗎?此操作無法復原。', // 操作結果 + testing: '正在測試連線...', testSuccess: '{provider}連線測試成功', testFailed: '{provider}連線測試失敗:{error}', updateSuccess: '更新成功', diff --git a/packages/ui/src/types/vue-i18n.d.ts b/packages/ui/src/types/vue-i18n.d.ts index cc75df0f..29a1a8ef 100644 --- a/packages/ui/src/types/vue-i18n.d.ts +++ b/packages/ui/src/types/vue-i18n.d.ts @@ -85,6 +85,7 @@ declare module 'vue-i18n' { deleteConfirm: string; // Operation Results + testing: string; testSuccess: string; testFailed: string; updateSuccess: string;