Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,8 @@ MultiRow、RadioRow 的配置器与运行时复用字段组件,分别使用复
扁平 `ModelItem[]`,由选择器统一分组和展示供应商。已有快照未保存状态时保留可选行为,已保存的
状态按 `SelectModel` 的可用性规则处理。切换模型一次性回写 `model_id` 和深拷贝后的
`model_params_setting`;清空时回写空 ID 和空参数,避免修改原配置中的参数对象。
配置器为每个可选模型保存 `model_form_field` 参数表单快照;运行时快照非空时开启 `SelectModel`
参数入口,参数表单与默认值取自快照中的 `model_form_field` 和 `model_params_setting`,不请求接口。

Knowledge 配置器复用 `SelectKnowledgeDialog` 选择可选知识库,沿用文件夹、共享资源查询及
相同 Embedding 模型约束。打开时传入已选快照,确认后回写可选知识库并清理已取消 ID 对应的
Expand All @@ -733,8 +735,10 @@ Model 配置器通过 `getSelectModelList(query)` 注入加载可选模型,返
上层提供 `ModelApi.getModelListWithShared` 或工作流的 `store.force.getModelListWithShared`;
循环体转发同名注入。该名称用于数据加载能力,与业务 API 方法名区分。

Model 配置器的默认模型也使用 `SelectModel`,仅展示已选的可选模型,不开启参数设置入口。
选择时将模型 ID 转换为包含已配置参数的 `default_value` 对象,清空时重置为空对象。
Model 配置器的默认模型也使用 `SelectModel`,仅展示已选的可选模型,并开启参数设置入口。
选择时将模型 ID 转换为包含已配置参数的 `default_value` 对象,清空时重置为空对象;参数弹窗以该模型
快照中的参数为默认值,确认后同时回写 `default_value` 和对应可选模型的 `model_params_setting`。
编辑已有字段时为缺少 `model_form_field` 的可选模型补齐参数表单快照。

## 跨页面业务组件

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { inject, ref, useTemplateRef } from 'vue'
import { inject, nextTick, ref, useTemplateRef } from 'vue'
import { cloneDeep } from 'lodash'
import type { DynamicFormField } from '@/api/types'
import { MkDynamicsForm, type FormField } from '@/components/mk-dynamics-form'
Expand Down Expand Up @@ -41,6 +41,8 @@ function open(modelId: string, settings?: Record<string, unknown>) {
const validFields = new Set(formFields.value.map(({ field }) => field))
const savedSettings = Object.fromEntries(Object.entries(settings ?? {}).filter(([field]) => validFields.has(field)))
formData.value = { ...defaults, ...cloneDeep(savedSettings) }
// 弹窗内容打开即挂载,表单只在挂载时读取字段;字段异步返回后重新渲染
return nextTick(() => formRef.value?.render(formFields.value, formData.value))
})
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script setup lang="ts">
import type { ModelItem } from '@/api/types'
import type { DynamicFormValue } from '../../type'
import { computed, inject, ref } from 'vue'
import { computed, inject, provide, ref } from 'vue'
import { cloneDeep } from 'lodash'
import SelectModel from '@/components/business/select-model/index.vue'
import { providerList as providerOptions } from '../../items/model/provider-data'
import { MODEL_TYPE_LABELS } from '@/constants/model'
Expand Down Expand Up @@ -50,11 +51,16 @@ const selectedModelsOptions = computed(() => {
return rawModelOptions.value.filter((model: DynamicFormValue) => ids.includes(model.id))
})

function loadModelFormFields(modelId: string): Promise<DynamicFormValue[]> {
if (!getModelParamsForm) return Promise.resolve([])
// 请求层已解包为字段数组,兼容仍返回 { data } 的注入实现
return getModelParamsForm(modelId).then((res: DynamicFormValue) => (Array.isArray(res) ? res : res?.data) || [])
}

function fetchDefaultParams(modelId: string) {
if (!getModelParamsForm) return
getModelParamsForm(modelId).then((res: DynamicFormValue) => {
const formFields = res?.data || []
const defaults = (res?.data || [])
loadModelFormFields(modelId).then((formFields) => {
const defaults = formFields
.map((item: DynamicFormValue) => {
if (item.show_default_value === false) {
return { [item.field]: undefined }
Expand Down Expand Up @@ -106,6 +112,34 @@ const getProviderItem = (modelId: string) => {
return { model_id: modelId, model_params_setting: {} }
}

// 默认模型参数:参数弹窗的默认值使用该模型已保存的参数,确认后同步到可选模型快照。
function getSavedModelParamsForm(modelId: string) {
const provider = formValue.value.provider_list?.find((p: DynamicFormValue) => p.model_id === modelId)
const settings = provider?.model_params_setting ?? {}
const formFields: Promise<DynamicFormValue[]> = provider?.model_form_field?.length
? Promise.resolve(provider.model_form_field)
: loadModelFormFields(modelId)
return formFields.then((fields) =>
fields.map((field) =>
Object.prototype.hasOwnProperty.call(settings, field.field) ? { ...field, default_value: cloneDeep(settings[field.field]) } : field,
),
)
}
provide('getModelParamsForm', getSavedModelParamsForm)

const defaultModelParams = computed({
get: () => formValue.value.default_value?.model_params_setting ?? {},
set: (settings: Record<string, unknown>) => {
const modelId = formValue.value.default_value?.model_id
if (!modelId) return
formValue.value.default_value = { ...formValue.value.default_value, model_params_setting: settings }
// 切换默认模型时 SelectModel 会先清空参数再加载默认值,清空不覆盖快照中已保存的参数
if (!Object.keys(settings).length) return
const provider = formValue.value.provider_list?.find((p: DynamicFormValue) => p.model_id === modelId)
if (provider) provider.model_params_setting = cloneDeep(settings)
},
})

const defaultModelId = computed({
get: () => formValue.value.default_value?.model_id ?? '',
set: (modelId: string) => {
Expand Down Expand Up @@ -140,6 +174,13 @@ const render = (formData: DynamicFormValue) => {
if (formData.model_type) {
fetchModelByType(formData.model_type)
}
// 补齐缺少参数表单快照的旧数据,保留已保存的参数
formValue.value.provider_list.forEach((provider: DynamicFormValue) => {
if (provider.model_form_field?.length) return
loadModelFormFields(provider.model_id).then((formFields) => {
provider.model_form_field = formFields
})
})
}

defineExpose({ getData, render })
Expand Down Expand Up @@ -171,6 +212,14 @@ defineExpose({ getData, render })
:rules="formValue.required ? [{ required: true, message: '请选择模型' }] : []"
v-if="formValue.provider_list && formValue.provider_list.length > 0"
>
<SelectModel v-model="defaultModelId" placeholder="请选择模型" :options="selectedModelsOptions" :provider-options="providerOptions" teleported />
<SelectModel
v-model="defaultModelId"
v-model:model-params="defaultModelParams"
can-edit-params
placeholder="请选择模型"
:options="selectedModelsOptions"
:provider-options="providerOptions"
teleported
/>
</el-form-item>
</template>
37 changes: 36 additions & 1 deletion ui/src/components/mk-dynamics-form/items/model/Model.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, provide, ref, watch } from 'vue'
import { cloneDeep } from 'lodash'
import { MODEL_STATUS } from '@/api/enums'
import type { Dict, ModelConfig, ModelItem } from '@/api/types'
Expand All @@ -14,6 +14,8 @@ interface ConfiguredModelOption {
model_name: string
provider: string
model_params_setting?: Dict<unknown>
/** 配置字段时保存的模型参数表单,运行时据此编辑参数,不再请求接口。 */
model_form_field?: FormField[]
status?: ModelItem['status']
source?: ModelItem['source']
}
Expand Down Expand Up @@ -41,7 +43,37 @@ const modelOptions = computed<ModelItem[]>(() =>
})),
)

// 模型参数:参数表单取自配置快照,默认值使用配置时保存的参数。
const canEditParams = computed(() => configuredModels.value.some((model) => model.model_form_field?.length))

function getModelParamsForm(modelId: string) {
const selectedModel = configuredModels.value.find((model) => model.model_id === modelId)
const settings = selectedModel?.model_params_setting ?? {}
return Promise.resolve(
(selectedModel?.model_form_field ?? []).map((field) =>
Object.prototype.hasOwnProperty.call(settings, field.field) ? { ...field, default_value: cloneDeep(settings[field.field]) } : field,
),
)
}
provide('getModelParamsForm', getModelParamsForm)

// SelectModel 切换模型后同步回写参数,此时 Props 尚未更新,记录当前选中的模型
const currentModelId = ref(props.modelValue?.model_id ?? '')
watch(
() => props.modelValue?.model_id,
(modelId) => {
currentModelId.value = modelId ?? ''
},
)

function handleModelParamsChange(settings: Dict<unknown>) {
if (!currentModelId.value) return
emit('update:modelValue', { model_id: currentModelId.value, model_params_setting: settings })
emit('change', props.formField)
}

function handleModelChange(modelId: string) {
currentModelId.value = modelId
const selectedModel = configuredModels.value.find((model) => model.model_id === modelId)
emit('update:modelValue', {
model_id: modelId,
Expand All @@ -55,8 +87,11 @@ function handleModelChange(modelId: string) {
<SelectModel
v-bind="$attrs"
:model-value="props.modelValue?.model_id ?? ''"
:model-params="props.modelValue?.model_params_setting ?? {}"
:can-edit-params="canEditParams"
:options="modelOptions"
:provider-options="providerList"
@update:model-value="handleModelChange"
@update:model-params="handleModelParamsChange"
/>
</template>
18 changes: 16 additions & 2 deletions ui/src/conversation-panel/components/message-input/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IMAGE_EXTENSIONS, DOCUMENT_EXTENSIONS, VIDEO_EXTENSIONS, AUDIO_EXTENSIO
import { isImage, isDocument, isAudio, isVideo } from '@/utils/icon'
import type { Dict } from '@/api/types'
import type { ChatMessage, Conversation, ConversationAppInfo, SendMessageOptions } from '../../core/types'
import { createUserFormStore } from './user-form/index'

export interface FileItem {
uid: number
Expand Down Expand Up @@ -50,6 +51,8 @@ export interface MessageInputDeps {
appInfo: Readonly<Ref<ConversationAppInfo | null>>
/** 禁用输入与发送,如门户没有可用智能体时;不传时始终可用。 */
disabled?: Readonly<Ref<boolean>>
/** 是否在用户输入表单中编辑接口传参,仅调试对话开启。 */
apiInputEditable?: boolean
}

// 上传请求返回前进度条的最大值
Expand All @@ -61,6 +64,7 @@ const UPLOAD_PROGRESS_PENDING_MAX = 95
export function createMessageInputStore(deps: MessageInputDeps) {
const { conversations, getChatId, renameChat, composerResetSignal, messages, streamLoading, sendMessage, uploadFile, stop, appInfo } = deps
const disabled = computed(() => deps.disabled?.value ?? false)
const userForm = createUserFormStore({ appInfo, apiInputEditable: deps.apiInputEditable ?? false })

// 输入状态与附件限制
const question = ref('')
Expand Down Expand Up @@ -187,7 +191,7 @@ export function createMessageInputStore(deps: MessageInputDeps) {
newQuestion: true,
questionContent: { ...message },
reChat: false,
formData,
formData: { ...userForm.getFormData(), ...formData },
onComplete: (e) => {
if (!e && isFirstMessage && questionText) renameChat(chatId, abstract).catch(() => {})
},
Expand All @@ -199,6 +203,12 @@ export function createMessageInputStore(deps: MessageInputDeps) {
if (!canSend.value) return
sending.value = true
try {
// 用户输入参数校验失败时由表单提示,保留输入内容与附件
const formValid = await userForm.validate().then(
() => true,
() => false,
)
if (!formValid) return
// 一次遍历完成附件分类,仅提交有附件的字段。
const message: QuestionMessage = { type: 'QUESTION', content: question.value.trim() }
for (const attachment of fileList.value) {
Expand All @@ -225,7 +235,10 @@ export function createMessageInputStore(deps: MessageInputDeps) {
/** 由开场白、回答中的快捷问题或 HTML 渲染发起提问;不读取也不清空输入框与暂存附件。 */
const sendQuestion = (content: string, options: QuestionOptions = {}) => {
if (disabled.value || streamLoading.value || sending.value || !content.trim()) return
submitQuestion({ ...options.attachments, type: 'QUESTION', content: content.trim() }, options.formData)
userForm
.validate()
.then(() => submitQuestion({ ...options.attachments, type: 'QUESTION', content: content.trim() }, options.formData))
.catch(() => {})
}

// 新建/切换会话时清空暂存文件与输入
Expand All @@ -239,6 +252,7 @@ export function createMessageInputStore(deps: MessageInputDeps) {

return {
appInfo,
userForm,
question,
fileList,
maxFiles,
Expand Down
4 changes: 3 additions & 1 deletion ui/src/conversation-panel/components/message-input/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useInputEvents } from '../../core/input-events'
import { useMessageInputStore, type FileItem } from './index'
import FileUploadCard from '@/components/mk-drag-upload/FileUploadCard.vue'
import ImageUploadCard from '@/components/mk-drag-upload/ImageUploadCard.vue'
import UserForm from './user-form/index.vue'

const {
question,
Expand Down Expand Up @@ -111,7 +112,8 @@ function getAttachmentDescription(file: FileItem) {
/>

<!-- 操作栏 -->
<div class="text-right mt-3">
<div class="mt-3 flex items-center">
<UserForm class="mr-3" />
<input ref="fileInputRef" type="file" multiple :accept="acceptList" class="hidden" @change="handleFileSelect" />
<MkTooltip placement="top">
<template #content>
Expand Down
Loading
Loading