diff --git a/ui/src/api/enums/tool.ts b/ui/src/api/enums/tool.ts index 0d813184951..187edf89932 100644 --- a/ui/src/api/enums/tool.ts +++ b/ui/src/api/enums/tool.ts @@ -1,4 +1,4 @@ -/** TODO 后端 Workspace 工具作用域枚举值。 */ +/** 后端 Workspace 工具作用域枚举值。 */ export const TOOL_SCOPE = { INTERNAL: 'INTERNAL', SHARED: 'SHARED', WORKSPACE: 'WORKSPACE' } as const /** 后端 Workspace 工具类型枚举值。 */ diff --git a/ui/src/workflow-canvas/WORKFLOW_README.md b/ui/src/workflow-canvas/WORKFLOW_README.md index 2d18744843a..a28440cbf71 100644 --- a/ui/src/workflow-canvas/WORKFLOW_README.md +++ b/ui/src/workflow-canvas/WORKFLOW_README.md @@ -128,6 +128,12 @@ Vue Teleport 节点继承页面上下文;画布核心不负责模型参数接 保留失效引用检查;节点级 `validate()` 统一调用表单校验。所有非登录表单仅使用 `@submit.prevent`, 保存或添加由按钮触发。 +基本信息节点的用户输入、接口传参和会话变量分别由 `UserInputTable`、`ApiParameterTable`、 +`ConversationVariableTable` 通过 `v-model` 编辑,使用小尺寸 `MkTable` 支持排序。字段弹窗只负责 +收集和校验单个字段,通过 `submit(data, index?)` 提交;表格完成重名检查和写回后调用 `close()`。 +节点入口深拷贝写回列表并发送原有字段刷新事件,保留显隐条件引用校验。用户输入与接口传参继续 +交叉检查参数重名;`UserInputSettingDialog` 独立维护直接展示参数设置,删除字段时由表格清理对应设置。 + 表单收集节点的 `component/form-setting/FormSettingTable` 通过 `v-model` 编辑动态表单的 `FormField[]`,负责增删改、排序和重名检查;`FormFieldDialog` 复用 `MkDynamicsFormConstructor`, 通过 `submit(data, index?)` 提交,由表格写回后调用 `close()`,关闭动画结束时重置编辑状态。 diff --git a/ui/src/workflow-canvas/core/NodeCascader.vue b/ui/src/workflow-canvas/core/NodeCascader.vue index 39f6eaf6664..23aefe21e26 100644 --- a/ui/src/workflow-canvas/core/NodeCascader.vue +++ b/ui/src/workflow-canvas/core/NodeCascader.vue @@ -85,7 +85,7 @@ function validate() { const fieldGroup = getOptionsValue().find(({ value }) => value === nodeId) if (!fieldGroup?.children?.some(({ value }) => value === fieldValue)) { selectedValue.value = [] - return Promise.reject('不存在的引用变量') + return Promise.reject('引用变量不存在') } return Promise.resolve() } diff --git a/ui/src/workflow-canvas/icons/reply-node-icon.vue b/ui/src/workflow-canvas/icons/reply-node-icon.vue index fec0921d630..78bb9e5b074 100644 --- a/ui/src/workflow-canvas/icons/reply-node-icon.vue +++ b/ui/src/workflow-canvas/icons/reply-node-icon.vue @@ -1,6 +1,6 @@ diff --git a/ui/src/workflow-canvas/nodes/ai-chat-node/index.vue b/ui/src/workflow-canvas/nodes/ai-chat-node/index.vue index b76d0453686..5c0d0322142 100644 --- a/ui/src/workflow-canvas/nodes/ai-chat-node/index.vue +++ b/ui/src/workflow-canvas/nodes/ai-chat-node/index.vue @@ -134,6 +134,7 @@ function validateModel(_rule: unknown, _value: unknown, callback: (error?: Error } function validate() { + // TODO 图片选择需要必填?为什么没有必填标记? return Promise.all([formData.value.vision ? imageCascaderRef.value?.validate() : Promise.resolve(), formRef.value?.validate()]).catch((error) => Promise.reject({ node: model, errMessage: error }), ) diff --git a/ui/src/workflow-canvas/nodes/base-node/component/api-parameter/ApiParameterDialog.vue b/ui/src/workflow-canvas/nodes/base-node/component/api-parameter/ApiParameterDialog.vue new file mode 100644 index 00000000000..af641697677 --- /dev/null +++ b/ui/src/workflow-canvas/nodes/base-node/component/api-parameter/ApiParameterDialog.vue @@ -0,0 +1,73 @@ + + + diff --git a/ui/src/workflow-canvas/nodes/base-node/component/api-parameter/ApiParameterTable.vue b/ui/src/workflow-canvas/nodes/base-node/component/api-parameter/ApiParameterTable.vue new file mode 100644 index 00000000000..53518498d18 --- /dev/null +++ b/ui/src/workflow-canvas/nodes/base-node/component/api-parameter/ApiParameterTable.vue @@ -0,0 +1,69 @@ + + + diff --git a/ui/src/workflow-canvas/nodes/base-node/component/api-parameter/index.vue b/ui/src/workflow-canvas/nodes/base-node/component/api-parameter/index.vue deleted file mode 100644 index 2d95f677ddb..00000000000 --- a/ui/src/workflow-canvas/nodes/base-node/component/api-parameter/index.vue +++ /dev/null @@ -1,102 +0,0 @@ - - - diff --git a/ui/src/workflow-canvas/nodes/base-node/component/conversation-variable/ConversationVariableDialog.vue b/ui/src/workflow-canvas/nodes/base-node/component/conversation-variable/ConversationVariableDialog.vue new file mode 100644 index 00000000000..6d32635a1c5 --- /dev/null +++ b/ui/src/workflow-canvas/nodes/base-node/component/conversation-variable/ConversationVariableDialog.vue @@ -0,0 +1,70 @@ + + + diff --git a/ui/src/workflow-canvas/nodes/base-node/component/conversation-variable/ConversationVariableTable.vue b/ui/src/workflow-canvas/nodes/base-node/component/conversation-variable/ConversationVariableTable.vue new file mode 100644 index 00000000000..ac122e1e960 --- /dev/null +++ b/ui/src/workflow-canvas/nodes/base-node/component/conversation-variable/ConversationVariableTable.vue @@ -0,0 +1,60 @@ + + + diff --git a/ui/src/workflow-canvas/nodes/base-node/component/conversation-variable/index.vue b/ui/src/workflow-canvas/nodes/base-node/component/conversation-variable/index.vue deleted file mode 100644 index 97129bc27a6..00000000000 --- a/ui/src/workflow-canvas/nodes/base-node/component/conversation-variable/index.vue +++ /dev/null @@ -1,88 +0,0 @@ - - - diff --git a/ui/src/workflow-canvas/nodes/base-node/component/user-input/UserInputFieldDialog.vue b/ui/src/workflow-canvas/nodes/base-node/component/user-input/UserInputFieldDialog.vue new file mode 100644 index 00000000000..8b39bac94cf --- /dev/null +++ b/ui/src/workflow-canvas/nodes/base-node/component/user-input/UserInputFieldDialog.vue @@ -0,0 +1,80 @@ + + + diff --git a/ui/src/workflow-canvas/nodes/base-node/component/user-input/UserInputSettingDialog.vue b/ui/src/workflow-canvas/nodes/base-node/component/user-input/UserInputSettingDialog.vue new file mode 100644 index 00000000000..433ad419538 --- /dev/null +++ b/ui/src/workflow-canvas/nodes/base-node/component/user-input/UserInputSettingDialog.vue @@ -0,0 +1,61 @@ + + + diff --git a/ui/src/workflow-canvas/nodes/base-node/component/user-input/UserInputTable.vue b/ui/src/workflow-canvas/nodes/base-node/component/user-input/UserInputTable.vue new file mode 100644 index 00000000000..bcedf60416f --- /dev/null +++ b/ui/src/workflow-canvas/nodes/base-node/component/user-input/UserInputTable.vue @@ -0,0 +1,112 @@ + + + diff --git a/ui/src/workflow-canvas/nodes/base-node/component/user-input/constant.ts b/ui/src/workflow-canvas/nodes/base-node/component/user-input/constant.ts new file mode 100644 index 00000000000..9a9e1ca2ffe --- /dev/null +++ b/ui/src/workflow-canvas/nodes/base-node/component/user-input/constant.ts @@ -0,0 +1 @@ +export const exposedInputTypes = ['TextInput', 'TextareaInput', 'PasswordInput', 'SingleSelect', 'MultiSelect', 'DatePicker', 'SwitchInput'] diff --git a/ui/src/workflow-canvas/nodes/base-node/component/user-input/index.vue b/ui/src/workflow-canvas/nodes/base-node/component/user-input/index.vue deleted file mode 100644 index 705c837d44a..00000000000 --- a/ui/src/workflow-canvas/nodes/base-node/component/user-input/index.vue +++ /dev/null @@ -1,172 +0,0 @@ - - - diff --git a/ui/src/workflow-canvas/nodes/base-node/index.vue b/ui/src/workflow-canvas/nodes/base-node/index.vue index edc0aad584c..bd3396a01ca 100644 --- a/ui/src/workflow-canvas/nodes/base-node/index.vue +++ b/ui/src/workflow-canvas/nodes/base-node/index.vue @@ -7,10 +7,11 @@ import ModelSelect from '@/components/business/model-select/index.vue' import type { FormField } from '@/components/mk-dynamics-form' import NodeContainer from '@/workflow-canvas/core/node-container/index.vue' import type { WorkflowNodeModel } from '@/workflow-canvas/core/workflow-node' +import { handleNodeWheel } from '@/workflow-canvas/core/utils' import { useWorkflowStore } from '@/workflow-canvas/store' -import ApiParameter from './component/api-parameter/index.vue' -import ConversationVariable from './component/conversation-variable/index.vue' -import UserInput from './component/user-input/index.vue' +import ApiParameterTable from './component/api-parameter/ApiParameterTable.vue' +import ConversationVariableTable from './component/conversation-variable/ConversationVariableTable.vue' +import UserInputTable from './component/user-input/UserInputTable.vue' import { defaultFileUploadSetting } from './constant' import { type ApiInputField, type BaseNodeForm, type ChatInputField, type UserInputSetting } from './types' @@ -23,10 +24,7 @@ const store = useWorkflowStore(apiType) const formRef = useTemplateRef('formRef') -const sttModelOptions = ref([]) -const ttsModelOptions = ref([]) -const providerOptions = ref([]) - +// 基本信息与节点数据初始化 const defaultForm: BaseNodeForm = { desc: '', file_upload_enable: false, @@ -72,20 +70,37 @@ const formData = computed({ set: (value) => (model.properties.node_data = value), }) -function handleEditorWheel(event: WheelEvent) { - if (event.ctrlKey) event.preventDefault() - else event.stopPropagation() +// 长期记忆 +function changeLongTermEnabled(enabled: boolean | number | string) { + formData.value.long_term_enable = Boolean(enabled) + if (enabled && !formData.value.long_term_model_id_type) formData.value.long_term_model_id_type = 'default' + model.graphModel.eventCenter.emit('refreshLongTermConfig', undefined) } -// 子模块数据映射 -const userInputFields = computed(() => (model.properties.user_input_field_list ?? []) as FormField[]) -const apiInputFields = computed(() => (model.properties.api_input_field_list ?? []) as ApiInputField[]) -const conversationVariables = computed(() => (model.properties.chat_input_field_list ?? []) as ChatInputField[]) -const userInputSetting = computed(() => - cloneDeep((model.properties.user_input_field_list_setting as UserInputSetting | undefined) ?? { exposed_fields: [], menu_title: '更多设置' }), -) -const defaultSttModelSetting = computed(() => model.getDefaultModelConfig('STT')) -const defaultTtsModelSetting = computed(() => model.getDefaultModelConfig('TTS')) +// 文件上传 +function changeFileUploadEnabled(enabled: boolean | number | string) { + formData.value.file_upload_enable = Boolean(enabled) + if (enabled && !formData.value.file_upload_setting) { + formData.value.file_upload_setting = cloneDeep(defaultFileUploadSetting) + } + model.graphModel.eventCenter.emit('refreshFileUploadConfig', undefined) +} + +// 用户输入:字段、展示设置与显隐引用校验 +const userInputFields = computed({ + get: () => (model.properties.user_input_field_list ?? []) as FormField[], + set: (fields) => { + model.properties.user_input_field_list = cloneDeep(fields) + model.graphModel.eventCenter.emit('refreshFieldList', undefined) + }, +}) +const userInputSetting = computed({ + get: () => + cloneDeep((model.properties.user_input_field_list_setting as UserInputSetting | undefined) ?? { exposed_fields: [], menu_title: '用户输入' }), + set: (setting) => { + model.properties.user_input_field_list_setting = cloneDeep(setting) + }, +}) function validateUserFieldReferences() { for (const userField of userInputFields.value) { @@ -93,14 +108,56 @@ function validateUserFieldReferences() { if (!condition.field?.[0] || !condition.field?.[1]) continue const isCurrentForm = condition.field[0] === model.id || (model.id === 'base-node' && condition.field[0] === 'global') if (isCurrentForm && !userInputFields.value.some(({ field }) => field === condition.field[1])) { - return Promise.reject('用户输入参数引用了已删除的变量') + return Promise.reject('引用变量不存在') } } } return Promise.resolve() } +// 接口传参 +const apiInputFields = computed({ + get: () => (model.properties.api_input_field_list ?? []) as ApiInputField[], + set: (fields) => { + model.properties.api_input_field_list = cloneDeep(fields) + model.graphModel.eventCenter.emit('refreshFieldList', undefined) + }, +}) + +// 会话变量 +const conversationVariables = computed({ + get: () => (model.properties.chat_input_field_list ?? []) as ChatInputField[], + set: (fields) => { + model.properties.chat_input_field_list = cloneDeep(fields) + model.graphModel.eventCenter.emit('chatFieldList', undefined) + }, +}) + +// 语音输入 +const sttModelOptions = ref([]) +const defaultSttModelSetting = computed(() => model.getDefaultModelConfig('STT')) + +function changeSpeechInputEnabled(enabled: boolean | number | string) { + if (!enabled) formData.value.stt_model_id = '' + if (!formData.value.stt_model_id_type) formData.value.stt_model_id_type = 'default' +} + +// 语音播放 +const ttsModelOptions = ref([]) +const defaultTtsModelSetting = computed(() => model.getDefaultModelConfig('TTS')) + +function changeSpeechPlaybackEnabled(enabled: boolean | number | string) { + if (enabled) return + formData.value.tts_model_id = '' + formData.value.tts_type = 'BROWSER' +} + +// 语音模型共用的供应商选项 +const providerOptions = ref([]) + +// 节点统一校验 function validate() { + // TODO v2没有标记必填 但是需要校验 需要核对一下 if (formData.value.tts_model_enable && formData.value.tts_type === 'CUSTOM' && !formData.value.tts_model_id) { return Promise.reject({ node: model, errMessage: '请选择语音播放模型' }) } @@ -119,51 +176,6 @@ function validate() { return Promise.all([validateUserFieldReferences(), formRef.value?.validate()]).catch((error) => Promise.reject({ node: model, errMessage: error })) } -// 子模块数据更新 -function changeLongTermEnabled(enabled: boolean | number | string) { - formData.value.long_term_enable = Boolean(enabled) - if (enabled && !formData.value.long_term_model_id_type) formData.value.long_term_model_id_type = 'default' - model.graphModel.eventCenter.emit('refreshLongTermConfig', undefined) -} - -function changeFileUploadEnabled(enabled: boolean | number | string) { - formData.value.file_upload_enable = Boolean(enabled) - if (enabled && !formData.value.file_upload_setting) { - formData.value.file_upload_setting = cloneDeep(defaultFileUploadSetting) - } - model.graphModel.eventCenter.emit('refreshFileUploadConfig', undefined) -} - -function updateUserInputFields(fields: FormField[]) { - model.properties.user_input_field_list = fields - model.graphModel.eventCenter.emit('refreshFieldList', undefined) -} - -function updateUserInputSetting(setting: UserInputSetting) { - model.properties.user_input_field_list_setting = setting -} - -function updateApiInputFields(fields: ApiInputField[]) { - model.properties.api_input_field_list = fields - model.graphModel.eventCenter.emit('refreshFieldList', undefined) -} - -function updateConversationVariables(fields: ChatInputField[]) { - model.properties.chat_input_field_list = fields - model.graphModel.eventCenter.emit('chatFieldList', undefined) -} - -function changeSpeechInputEnabled(enabled: boolean | number | string) { - if (!enabled) formData.value.stt_model_id = '' - if (!formData.value.stt_model_id_type) formData.value.stt_model_id_type = 'default' -} - -function changeSpeechPlaybackEnabled(enabled: boolean | number | string) { - if (enabled) return - formData.value.tts_model_id = '' - formData.value.tts_type = 'BROWSER' -} - onMounted(() => { model.validate = validate if (!Array.isArray(model.properties.user_input_field_list)) model.properties.user_input_field_list = [] @@ -195,7 +207,7 @@ onMounted(() => { - + @@ -234,20 +246,18 @@ onMounted(() => { - - - - - - - + + + + + + + + + + + +