diff --git a/src/views/settings/workflow/component/FormFieldsEditor.vue b/src/views/settings/workflow/component/FormFieldsEditor.vue index ed098a9..5292248 100644 --- a/src/views/settings/workflow/component/FormFieldsEditor.vue +++ b/src/views/settings/workflow/component/FormFieldsEditor.vue @@ -33,6 +33,9 @@ import { ref, watch, nextTick } from 'vue'; export interface FormField { + // 不可变字段标识(保存/下游引用的 key);label 仅为展示名,重命名不影响已建立的引用。 + // 旧数据缺省时退化用 label,保存一次后 field 固化不再随 label 变化 + field?: string; label: string; type: string; // input | number | textarea | switch | uploadMultiple value: any; // 默认值(uploadMultiple 时存文件 URL 数组或空) @@ -67,8 +70,12 @@ const sync = () => { emit('update:modelValue', clone(fields.value)); }; +// 字段唯一标识生成:时间戳 + 递增序号,避免同节点内字段 field 冲突 +let fieldSeq = 0; +const genFieldKey = () => `f_${Date.now().toString(36)}_${++fieldSeq}`; + const addField = () => { - fields.value.push({ label: '', type: 'input', value: '', required: false }); + fields.value.push({ field: genFieldKey(), label: '', type: 'input', value: '', required: false }); sync(); }; diff --git a/src/views/settings/workflow/component/modelParamUtils.ts b/src/views/settings/workflow/component/modelParamUtils.ts index 9a70e33..28cf8f0 100644 --- a/src/views/settings/workflow/component/modelParamUtils.ts +++ b/src/views/settings/workflow/component/modelParamUtils.ts @@ -373,7 +373,14 @@ export function attachFormFieldValueSources(params: any, fields: ExposedField[] if (!f || typeof f.path !== 'string') continue; const cur = resolvePath(params, f.path); if (cur && typeof cur === 'object' && !Array.isArray(cur)) { - cur.valueSource = [{ nodeId: startNodeId, field: f.path }]; + // 已有指向上游节点的真实引用时保持互斥:不覆盖为开始节点标记,避免引用丢失 + const vs = cur.valueSource; + const hasRealRef = Array.isArray(vs) + ? vs.some((r: any) => r && r.nodeId && r.nodeId !== startNodeId) + : !!vs && typeof vs === 'object' && vs.nodeId && vs.nodeId !== startNodeId; + if (!hasRealRef) { + cur.valueSource = [{ nodeId: startNodeId, field: f.path }]; + } } } } diff --git a/src/views/settings/workflow/index.vue b/src/views/settings/workflow/index.vue index a1813f5..00264ae 100644 --- a/src/views/settings/workflow/index.vue +++ b/src/views/settings/workflow/index.vue @@ -280,6 +280,47 @@ const selectedSkillData = ref(null); const showWorkflowSelector = ref(false); const selectedWorkflowData = ref(null); +// 过滤对象(叶子 def / formConfig 条目等)上指向已删除节点的 valueSource 引用;返回是否有变化。 +// 兼容单对象与数组两种 valueSource 形态;并递归 modelRequestParams 深树(对象 attrs / 数组模板 enumValues / value 实例)。 +const filterValueSourceRefs = (obj: any, deletedNodeId: string): boolean => { + if (!obj || typeof obj !== 'object') return false; + let changed = false; + const vs = obj.valueSource; + if (vs && typeof vs === 'object') { + const arr = Array.isArray(vs) ? vs : [vs]; + const keep = arr.filter((r: any) => r && r.nodeId !== deletedNodeId); + if (keep.length !== arr.length) { + if (keep.length === 0) delete obj.valueSource; + else obj.valueSource = keep; + changed = true; + } + } + if (obj.attrs && typeof obj.attrs === 'object' && !Array.isArray(obj.attrs)) { + if (filterValueSourceRefs(obj.attrs, deletedNodeId)) changed = true; + } + if (Array.isArray(obj.enumValues)) { + for (const tpl of obj.enumValues) { + if (filterValueSourceRefs(tpl, deletedNodeId)) changed = true; + } + } + if (Array.isArray(obj.value)) { + for (const v of obj.value) { + if (filterValueSourceRefs(v, deletedNodeId)) changed = true; + } + } + return changed; +}; + +// 清理数组中每项指向已删除节点的引用(formConfig / modelRequestParamsPath / subFlowConfig.fields) +const filterValueSourceInArray = (arr: any[] | null | undefined, deletedNodeId: string): boolean => { + if (!Array.isArray(arr)) return false; + let changed = false; + for (const item of arr) { + if (item && typeof item === 'object' && filterValueSourceRefs(item, deletedNodeId)) changed = true; + } + return changed; +}; + const deleteSelectedNode = async () => { if (!selectedNode.value?.data) return; @@ -295,8 +336,30 @@ const deleteSelectedNode = async () => { type: 'warning', }); - removeNodes([selectedNode.value.id]); + const deletedNodeId = selectedNode.value.id; + removeNodes([deletedNodeId]); selectedNode.value = null; + + // 清理其它节点指向被删节点的引用,避免保存脏 DSL(懒清理 watch 只在重新渲染时生效,这里主动清) + for (const node of nodes.value) { + if (node.id === deletedNodeId || !node.data) continue; + const data = node.data; + let changed = false; + if (filterValueSourceInArray(data.formConfig, deletedNodeId)) changed = true; + const mc = data.modelConfig; + if (mc) { + if (filterValueSourceInArray(mc.modelRequestParamsPath, deletedNodeId)) changed = true; + if (filterValueSourceRefs(mc.modelRequestParams, deletedNodeId)) changed = true; + } + if (data.subFlowConfig && filterValueSourceInArray(data.subFlowConfig.fields, deletedNodeId)) changed = true; + if (changed) { + // 同步 VueFlow 内部状态 + 触发节点重新渲染 + updateNode(node.id, node); + const idx = nodes.value.findIndex((n) => n.id === node.id); + if (idx >= 0) nodes.value[idx] = { ...node, data: { ...data } }; + } + } + ElMessage.success('节点已删除'); } catch { // 用户取消删除 @@ -309,6 +372,11 @@ const onConnect = (connection: Connection) => { if (!source?.data || !target?.data) return; + if (connection.source === connection.target) { + ElMessage.warning('不能将节点连接到自身'); + return; + } + if (target.data.nodeCode === '__start__') { ElMessage.warning('开始节点不能被连接'); return; @@ -916,6 +984,32 @@ const validateFlowConstraints = () => { return { ok: false, message: '工作流必须包含开始节点' }; } + // 自环校验(onConnect 已拦截,此处兜底历史/加载数据) + for (const edge of currentEdges) { + if (edge.source === edge.target) { + return { ok: false, message: '存在连接节点自身的连线,请先删除该连线' }; + } + } + + // model 节点必须已选模型,否则后端执行期才报错 + const unconfiguredModel = currentNodes.find( + (n) => n.data?.nodeCode === 'model' && !(n.data?.modelConfig && n.data.modelConfig.modelId) + ); + if (unconfiguredModel) { + const label = unconfiguredModel.data?.label || unconfiguredModel.id; + return { ok: false, message: `节点「${label}」尚未选择模型,请先配置` }; + } + + // 孤立节点(无任何连线、非开始节点):不参与执行,视为绘制遗漏 + const isolatedNode = currentNodes.find((n) => { + if (isStartNode(n)) return false; + return !currentEdges.some((e) => e.source === n.id || e.target === n.id); + }); + if (isolatedNode) { + const label = isolatedNode.data?.label || isolatedNode.id; + return { ok: false, message: `存在未连接的节点「${label}」,请先连接或删除` }; + } + // 检查边的约束 for (const edge of currentEdges) { const targetNode = nodeMap.get(edge.target); @@ -1163,7 +1257,8 @@ const buildOutputConfig = (node: Node, startNodeId = '', outputIndex?: const fieldName = f.field || f.label || ''; return { type: f.type || 'input', - field: f.label || f.field || '', + // field 为不可变标识(下游引用 key),label 仅为展示名;field 优先,避免重命名 label 使引用失效 + field: f.field || f.label || '', label: f.label || f.field || '', value: f.value ?? '', required: Boolean(f.required), @@ -1538,17 +1633,23 @@ const confirmSaveWorkflow = async () => { }); ElMessage.success('工作流更新成功'); } else { - await saveWorkflow({ + const res = await saveWorkflow({ flowName: saveForm.value.flowName, description: saveForm.value.description, flowContent: workflowDsl, }); ElMessage.success('工作流保存成功'); + // 新建成功:回填后端返回的 id,使后续「再点保存」走更新而非新建重复副本 + const newData = (res as any)?.data; + const newId = + typeof newData === 'string' + ? newData + : newData?.id ?? newData?.flowId ?? newData?.data?.id ?? newData?.data?.flowId ?? ''; + if (newId) currentEditingWorkflowId.value = String(newId); } saveDialogVisible.value = false; - saveForm.value.flowName = ''; - saveForm.value.description = ''; - currentEditingWorkflowId.value = null; + // 保存成功后保留 saveForm(名称/描述)与 currentEditingWorkflowId,便于连续编辑再保存; + // 新建/编辑入口(createNewWorkflow / editWorkflow)会重置这两者 await fetchWorkflowList(); } catch { // 错误已由全局拦截器处理