mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
fix(models,settings,conversation): scope model_id uniqueness per
provider and harden model reference resolution
This commit is contained in:
@@ -229,12 +229,15 @@ import { inject, computed, watch, nextTick, type Ref, ref } from 'vue'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import z from 'zod'
|
||||
import { useMutation, useQueryCache } from '@pinia/colada'
|
||||
import { postModels, putModelsModelByModelId } from '@memoh/sdk'
|
||||
import { postModels, putModelsById, putModelsModelByModelId } from '@memoh/sdk'
|
||||
import type { ModelsGetResponse } from '@memoh/sdk'
|
||||
import { CLIENT_TYPE_LIST, CLIENT_TYPE_META } from '@/constants/client-types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
const availableInputModalities = ['text', 'image', 'audio', 'video', 'file'] as const
|
||||
const selectedModalities = ref<string[]>(['text'])
|
||||
const { t } = useI18n()
|
||||
|
||||
const formSchema = toTypedSchema(z.object({
|
||||
type: z.string().min(1),
|
||||
@@ -313,6 +316,17 @@ const { mutateAsync: createModel, isLoading: createLoading } = useMutation({
|
||||
onSettled: () => queryCache.invalidateQueries({ key: ['provider-models'] }),
|
||||
})
|
||||
const { mutateAsync: updateModel, isLoading: updateLoading } = useMutation({
|
||||
mutation: async ({ id, data }: { id: string; data: Record<string, unknown> }) => {
|
||||
const { data: result } = await putModelsById({
|
||||
path: { id },
|
||||
body: data as any,
|
||||
throwOnError: true,
|
||||
})
|
||||
return result
|
||||
},
|
||||
onSettled: () => queryCache.invalidateQueries({ key: ['provider-models'] }),
|
||||
})
|
||||
const { mutateAsync: updateModelByLegacyModelID, isLoading: updateLegacyLoading } = useMutation({
|
||||
mutation: async ({ modelId, data }: { modelId: string; data: Record<string, unknown> }) => {
|
||||
const { data: result } = await putModelsModelByModelId({
|
||||
path: { modelId },
|
||||
@@ -323,7 +337,7 @@ const { mutateAsync: updateModel, isLoading: updateLoading } = useMutation({
|
||||
},
|
||||
onSettled: () => queryCache.invalidateQueries({ key: ['provider-models'] }),
|
||||
})
|
||||
const isLoading = computed(() => createLoading.value || updateLoading.value)
|
||||
const isLoading = computed(() => createLoading.value || updateLoading.value || updateLegacyLoading.value)
|
||||
|
||||
async function addModel(e: Event) {
|
||||
e.preventDefault()
|
||||
@@ -366,16 +380,31 @@ async function addModel(e: Event) {
|
||||
}
|
||||
|
||||
if (isEdit) {
|
||||
await updateModel({ modelId: fallback!.model_id, data: payload as any })
|
||||
const modelUUID = fallback?.id
|
||||
if (modelUUID) {
|
||||
await updateModel({ id: modelUUID, data: payload as any })
|
||||
} else {
|
||||
await updateModelByLegacyModelID({ modelId: fallback!.model_id, data: payload as any })
|
||||
}
|
||||
} else {
|
||||
await createModel(payload as any)
|
||||
}
|
||||
open.value = false
|
||||
} catch {
|
||||
} catch (error) {
|
||||
toast.error(resolveErrorMessage(error, t('common.saveFailed')))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function resolveErrorMessage(error: unknown, fallback: string): string {
|
||||
if (error instanceof Error && error.message.trim()) return error.message
|
||||
if (error && typeof error === 'object' && 'message' in error) {
|
||||
const msg = (error as { message?: string }).message
|
||||
if (msg && msg.trim()) return msg
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
watch(open, async () => {
|
||||
if (!open.value) {
|
||||
title.value = 'title'
|
||||
|
||||
@@ -52,13 +52,13 @@
|
||||
</div>
|
||||
<button
|
||||
v-for="model in group.models"
|
||||
:key="model.model_id"
|
||||
:key="model.id || `${model.llm_provider_id}:${model.model_id}`"
|
||||
class="relative flex w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground"
|
||||
:class="{ 'bg-accent': selected === model.model_id }"
|
||||
@click="selectModel(model.model_id)"
|
||||
:class="{ 'bg-accent': selected === model.id }"
|
||||
@click="selectModel(model.id)"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
v-if="selected === model.model_id"
|
||||
v-if="selected === model.id"
|
||||
:icon="['fas', 'check']"
|
||||
class="size-3.5"
|
||||
/>
|
||||
@@ -145,11 +145,12 @@ const filteredGroups = computed(() => {
|
||||
// 显示选中模型的名称
|
||||
const displayLabel = computed(() => {
|
||||
if (!selected.value) return ''
|
||||
const model = typeFilteredModels.value.find((m) => m.model_id === selected.value)
|
||||
const model = typeFilteredModels.value.find((m) => m.id === selected.value)
|
||||
return model?.name || model?.model_id || selected.value
|
||||
})
|
||||
|
||||
function selectModel(modelId: string) {
|
||||
function selectModel(modelId?: string) {
|
||||
if (!modelId) return
|
||||
selected.value = modelId
|
||||
open.value = false
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Item variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>{{ model.name }}</ItemTitle>
|
||||
<ItemTitle>{{ model.name || model.model_id }}</ItemTitle>
|
||||
<ItemDescription class="gap-2 flex flex-wrap items-center mt-3">
|
||||
<Badge variant="outline">
|
||||
{{ model.type }}
|
||||
@@ -26,7 +26,7 @@
|
||||
<ConfirmPopover
|
||||
:message="$t('models.deleteModelConfirm')"
|
||||
:loading="deleteLoading"
|
||||
@confirm="$emit('delete', model.name)"
|
||||
@confirm="$emit('delete', model.id ?? '')"
|
||||
>
|
||||
<template #trigger>
|
||||
<Button variant="outline">
|
||||
@@ -58,6 +58,6 @@ defineProps<{
|
||||
|
||||
defineEmits<{
|
||||
edit: [model: ModelsGetResponse]
|
||||
delete: [name: string]
|
||||
delete: [id: string]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
>
|
||||
<ModelItem
|
||||
v-for="model in models"
|
||||
:key="model.model_id"
|
||||
:key="model.id || `${model.llm_provider_id}:${model.model_id}`"
|
||||
:model="model"
|
||||
:delete-loading="deleteModelLoading"
|
||||
@edit="(model) => $emit('edit', model)"
|
||||
@delete="(name) => $emit('delete', name)"
|
||||
@delete="(id) => $emit('delete', id)"
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -61,6 +61,6 @@ defineProps<{
|
||||
|
||||
defineEmits<{
|
||||
edit: [model: ModelsGetResponse]
|
||||
delete: [name: string]
|
||||
delete: [id: string]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -33,7 +33,7 @@ import ProviderForm from './components/provider-form.vue'
|
||||
import ModelList from './components/model-list.vue'
|
||||
import { computed, inject, provide, reactive, ref, toRef, watch } from 'vue'
|
||||
import { useQuery, useMutation, useQueryCache } from '@pinia/colada'
|
||||
import { putProvidersById, deleteProvidersById, getProvidersByIdModels, deleteModelsModelByModelId } from '@memoh/sdk'
|
||||
import { putProvidersById, deleteProvidersById, getProvidersByIdModels, deleteModelsById } from '@memoh/sdk'
|
||||
import type { ModelsGetResponse, ProvidersGetResponse } from '@memoh/sdk'
|
||||
|
||||
// ---- Model 编辑状态(provide 给 CreateModel) ----
|
||||
@@ -86,8 +86,9 @@ const { mutate: changeProvider, isLoading: editLoading } = useMutation({
|
||||
})
|
||||
|
||||
const { mutate: deleteModel, isLoading: deleteModelLoading } = useMutation({
|
||||
mutation: async (modelName: string) => {
|
||||
await deleteModelsModelByModelId({ path: { modelId: modelName }, throwOnError: true })
|
||||
mutation: async (modelID: string) => {
|
||||
if (!modelID) return
|
||||
await deleteModelsById({ path: { id: modelID }, throwOnError: true })
|
||||
},
|
||||
onSettled: () => queryCache.invalidateQueries({ key: ['provider-models'] }),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user