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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ {{ editingIndex === undefined ? '添加' : '保存' }}
+
+
+
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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 取消
- {{ editingIndex === undefined ? '添加' : '保存' }}
-
-
-
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ {{ editingIndex === undefined ? '添加' : '保存' }}
+
+
+
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 @@
+
+
+
+
+
+ 会话变量
+
+
+
+
+
+ {{ row.field }}
+
+
+
+
+ {{ row.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 取消
- {{ editingIndex === undefined ? '添加' : '保存' }}
-
-
-
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 @@
+
+
+
+
+
+
+ 取消
+ {{ editingIndex === undefined ? '添加' : '保存' }}
+
+
+
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 @@
+
+
+
+
+
+
+
+
+ 外置参数设置(最多可显示3个)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 保存
+
+
+
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 @@
+
+
+
+
+
+
+
+
+ {{ row.field }}
+
+
+
+
+ {{
+ row.label.label
+ }}
+ {{ row.label }}
+
+
+
+ {{
+ dynamicFormTypeOptions.find(({ value }) => value === row.input_type)?.label ?? row.input_type
+ }}
+
+
+
+ {{ formatDefaultValue(row) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
-
-
-
-
-
-
-
-
- {{ formatLabel(row.label, row.field) }}
-
-
- {{ dynamicFormTypeOptions.find(({ value }) => value === row.input_type)?.label ?? row.input_type }}
-
-
- {{ formatDefaultValue(row) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 取消
- {{ editingIndex === undefined ? '添加' : '保存' }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 取消
- 保存
-
-
-
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(() => {
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+