feat: 优化工作流字段化参数配置
- 开始节点固定 user_input 并区分系统入口与自定义参数 - LLM 与知识库节点切换为字段值加上游引用配置 - 单节点调试改为字段预览与上游引用输入模式
This commit is contained in:
@@ -0,0 +1,471 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { Edge, Node } from '@xyflow/svelte';
|
||||
|
||||
import {
|
||||
buildAutoBindingPatch,
|
||||
buildSequentialFieldBindingPatches,
|
||||
buildFieldBindingPatch,
|
||||
buildEditorReferenceParameters,
|
||||
buildSingleRunModel,
|
||||
buildSingleRunParameters,
|
||||
createInitialWorkflowData,
|
||||
ensureStartNodeParameters,
|
||||
FIELD_BINDING_META_KEY,
|
||||
normalizeWorkflowStartNodes,
|
||||
} from './workflowNodeFields';
|
||||
|
||||
describe('workflow node fields', () => {
|
||||
it('creates initial workflow data with fixed start input', () => {
|
||||
const initial = createInitialWorkflowData();
|
||||
expect(initial.nodes).toHaveLength(1);
|
||||
expect(initial.nodes[0]?.type).toBe('startNode');
|
||||
|
||||
const parameters = ensureStartNodeParameters(
|
||||
(initial.nodes[0]?.data?.parameters || []) as any[],
|
||||
);
|
||||
expect(parameters).toHaveLength(1);
|
||||
expect(parameters[0]?.name).toBe('user_input');
|
||||
expect(parameters[0]?.systemReserved).toBe(true);
|
||||
expect(parameters[0]?.required).toBe(true);
|
||||
});
|
||||
|
||||
it('builds upstream reference candidates from start node', () => {
|
||||
const startNode: Node = {
|
||||
id: 'start_1',
|
||||
type: 'startNode',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
title: '流程开始',
|
||||
parameters: ensureStartNodeParameters(),
|
||||
},
|
||||
};
|
||||
const llmNode: Node = {
|
||||
id: 'llm_1',
|
||||
type: 'llmNode',
|
||||
position: { x: 120, y: 0 },
|
||||
data: {
|
||||
title: '大模型',
|
||||
parameters: [],
|
||||
},
|
||||
};
|
||||
const edges: Edge[] = [
|
||||
{ id: 'edge_1', source: 'start_1', target: 'llm_1' } as Edge,
|
||||
];
|
||||
|
||||
const parameters = buildEditorReferenceParameters(
|
||||
'llm_1',
|
||||
[startNode, llmNode],
|
||||
edges,
|
||||
[],
|
||||
);
|
||||
|
||||
expect(parameters.some((item) => item.name === 'start_1.user_input')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
parameters.find((item) => item.name === 'start_1.user_input')
|
||||
?.displayName,
|
||||
).toBe('流程开始 > 用户问题');
|
||||
});
|
||||
|
||||
it('applies default binding to llm user prompt after connect', () => {
|
||||
const startNode: Node = {
|
||||
id: 'start_1',
|
||||
type: 'startNode',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
title: '流程开始',
|
||||
parameters: ensureStartNodeParameters(),
|
||||
},
|
||||
};
|
||||
const llmNode: Node = {
|
||||
id: 'llm_1',
|
||||
type: 'llmNode',
|
||||
position: { x: 120, y: 0 },
|
||||
data: {
|
||||
title: '大模型',
|
||||
parameters: [],
|
||||
userPrompt: '',
|
||||
},
|
||||
};
|
||||
const edges: Edge[] = [
|
||||
{ id: 'edge_1', source: 'start_1', target: 'llm_1' } as Edge,
|
||||
];
|
||||
|
||||
const patch = buildAutoBindingPatch(llmNode, [startNode, llmNode], edges);
|
||||
expect(patch?.userPrompt).toBe('{{start_1.user_input}}');
|
||||
expect((patch?.parameters as any[])?.[0]?.name).toBe('start_1.user_input');
|
||||
expect((patch?.[FIELD_BINDING_META_KEY] as any)?.userPrompt?.userModified)
|
||||
.toBe(false);
|
||||
});
|
||||
|
||||
it('does not auto-bind from legacy start nodes without user_input', () => {
|
||||
const legacyStartNode: Node = {
|
||||
id: 'start_legacy',
|
||||
type: 'startNode',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
title: '开始节点',
|
||||
parameters: [],
|
||||
},
|
||||
};
|
||||
const llmNode: Node = {
|
||||
id: 'llm_1',
|
||||
type: 'llmNode',
|
||||
position: { x: 120, y: 0 },
|
||||
data: {
|
||||
title: '大模型',
|
||||
parameters: [],
|
||||
userPrompt: '',
|
||||
},
|
||||
};
|
||||
const edges: Edge[] = [
|
||||
{ id: 'edge_1', source: 'start_legacy', target: 'llm_1' } as Edge,
|
||||
];
|
||||
|
||||
expect(
|
||||
buildAutoBindingPatch(llmNode, [legacyStartNode, llmNode], edges),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('clears auto-filled start bindings after disconnect', () => {
|
||||
const startNode: Node = {
|
||||
id: 'start_1',
|
||||
type: 'startNode',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
title: '流程开始',
|
||||
parameters: ensureStartNodeParameters(),
|
||||
},
|
||||
};
|
||||
const llmNode: Node = {
|
||||
id: 'llm_1',
|
||||
type: 'llmNode',
|
||||
position: { x: 120, y: 0 },
|
||||
data: {
|
||||
title: '大模型',
|
||||
userPrompt: '{{start_1.user_input}}',
|
||||
parameters: [
|
||||
{
|
||||
name: 'start_1.user_input',
|
||||
ref: 'start_1.user_input',
|
||||
refType: 'ref',
|
||||
autoManaged: true,
|
||||
},
|
||||
],
|
||||
[FIELD_BINDING_META_KEY]: {
|
||||
userPrompt: {
|
||||
autoFilledFrom: 'start_1.user_input',
|
||||
userModified: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const patch = buildFieldBindingPatch(llmNode, [startNode, llmNode], []);
|
||||
expect(patch?.userPrompt).toBe('');
|
||||
expect(patch?.parameters).toEqual([]);
|
||||
expect(patch?.[FIELD_BINDING_META_KEY]).toEqual({});
|
||||
});
|
||||
|
||||
it('removes managed param for disconnected manual upstream refs so token becomes invalid', () => {
|
||||
const startNode: Node = {
|
||||
id: 'start_1',
|
||||
type: 'startNode',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
title: '流程开始',
|
||||
parameters: ensureStartNodeParameters(),
|
||||
},
|
||||
};
|
||||
const knowledgeNode: Node = {
|
||||
id: 'knowledge_1',
|
||||
type: 'knowledgeNode',
|
||||
position: { x: 80, y: 0 },
|
||||
data: {
|
||||
title: '知识库',
|
||||
outputDefs: [
|
||||
{
|
||||
name: 'documents',
|
||||
dataType: 'String',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const llmNode: Node = {
|
||||
id: 'llm_1',
|
||||
type: 'llmNode',
|
||||
position: { x: 120, y: 0 },
|
||||
data: {
|
||||
title: '大模型',
|
||||
systemPrompt: '{{knowledge_1.documents}}',
|
||||
userPrompt: '',
|
||||
parameters: [
|
||||
{
|
||||
name: 'knowledge_1.documents',
|
||||
ref: 'knowledge_1.documents',
|
||||
refType: 'ref',
|
||||
autoManaged: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const connectedEdges: Edge[] = [
|
||||
{ id: 'edge_1', source: 'start_1', target: 'knowledge_1' } as Edge,
|
||||
{ id: 'edge_2', source: 'knowledge_1', target: 'llm_1' } as Edge,
|
||||
];
|
||||
|
||||
const connectedParameters = buildEditorReferenceParameters(
|
||||
'llm_1',
|
||||
[startNode, knowledgeNode, llmNode],
|
||||
connectedEdges,
|
||||
(llmNode.data?.parameters || []) as any[],
|
||||
);
|
||||
expect(
|
||||
connectedParameters.some((item) => item.name === 'knowledge_1.documents'),
|
||||
).toBe(true);
|
||||
|
||||
const patch = buildFieldBindingPatch(
|
||||
llmNode,
|
||||
[startNode, knowledgeNode, llmNode],
|
||||
[],
|
||||
);
|
||||
expect((patch?.parameters as any[])?.[0]?.name).toBe('knowledge_1.documents');
|
||||
expect((patch?.parameters as any[])?.[0]?.disconnected).toBe(true);
|
||||
expect((patch?.parameters as any[])?.[0]?.displayName).toBe('documents');
|
||||
expect(patch).not.toHaveProperty('systemPrompt');
|
||||
});
|
||||
|
||||
it('restores auto-filled user input binding after reconnect through upstream chain', () => {
|
||||
const startNode: Node = {
|
||||
id: 'start_1',
|
||||
type: 'startNode',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
title: '流程开始',
|
||||
parameters: ensureStartNodeParameters(),
|
||||
},
|
||||
};
|
||||
const knowledgeNode: Node = {
|
||||
id: 'knowledge_1',
|
||||
type: 'knowledgeNode',
|
||||
position: { x: 80, y: 0 },
|
||||
data: {
|
||||
title: '知识库',
|
||||
keyword: '',
|
||||
parameters: [],
|
||||
outputDefs: [
|
||||
{
|
||||
name: 'documents',
|
||||
dataType: 'String',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const llmNode: Node = {
|
||||
id: 'llm_1',
|
||||
type: 'llmNode',
|
||||
position: { x: 120, y: 0 },
|
||||
data: {
|
||||
title: '大模型',
|
||||
userPrompt: '',
|
||||
parameters: [],
|
||||
},
|
||||
};
|
||||
const edges: Edge[] = [
|
||||
{ id: 'edge_1', source: 'start_1', target: 'knowledge_1' } as Edge,
|
||||
{ id: 'edge_2', source: 'knowledge_1', target: 'llm_1' } as Edge,
|
||||
];
|
||||
|
||||
const knowledgePatch = buildFieldBindingPatch(
|
||||
knowledgeNode,
|
||||
[startNode, knowledgeNode, llmNode],
|
||||
edges,
|
||||
);
|
||||
const nextKnowledgeNode: Node = {
|
||||
...knowledgeNode,
|
||||
data: {
|
||||
...knowledgeNode.data,
|
||||
...knowledgePatch,
|
||||
},
|
||||
};
|
||||
|
||||
const llmPatch = buildFieldBindingPatch(
|
||||
llmNode,
|
||||
[startNode, nextKnowledgeNode, llmNode],
|
||||
edges,
|
||||
);
|
||||
expect(knowledgePatch?.keyword).toBe('{{start_1.user_input}}');
|
||||
expect(llmPatch?.userPrompt).toBe('{{start_1.user_input}}');
|
||||
});
|
||||
|
||||
it('applies reconnect patches sequentially so downstream nodes can restore in the same batch', () => {
|
||||
const startNode: Node = {
|
||||
id: 'start_1',
|
||||
type: 'startNode',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
title: '流程开始',
|
||||
parameters: ensureStartNodeParameters(),
|
||||
},
|
||||
};
|
||||
const knowledgeNode: Node = {
|
||||
id: 'knowledge_1',
|
||||
type: 'knowledgeNode',
|
||||
position: { x: 80, y: 0 },
|
||||
data: {
|
||||
title: '知识库',
|
||||
keyword: '',
|
||||
parameters: [],
|
||||
outputDefs: [
|
||||
{
|
||||
name: 'documents',
|
||||
dataType: 'String',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const llmNode: Node = {
|
||||
id: 'llm_1',
|
||||
type: 'llmNode',
|
||||
position: { x: 120, y: 0 },
|
||||
data: {
|
||||
title: '大模型',
|
||||
userPrompt: '',
|
||||
parameters: [],
|
||||
},
|
||||
};
|
||||
const edges: Edge[] = [
|
||||
{ id: 'edge_1', source: 'start_1', target: 'knowledge_1' } as Edge,
|
||||
{ id: 'edge_2', source: 'knowledge_1', target: 'llm_1' } as Edge,
|
||||
];
|
||||
|
||||
const patches = buildSequentialFieldBindingPatches(
|
||||
['knowledge_1', 'llm_1'],
|
||||
[startNode, knowledgeNode, llmNode],
|
||||
edges,
|
||||
);
|
||||
|
||||
expect(patches).toHaveLength(2);
|
||||
expect(patches[0]).toMatchObject({
|
||||
nodeId: 'knowledge_1',
|
||||
patch: {
|
||||
keyword: '{{start_1.user_input}}',
|
||||
},
|
||||
});
|
||||
expect(patches[1]).toMatchObject({
|
||||
nodeId: 'llm_1',
|
||||
patch: {
|
||||
userPrompt: '{{start_1.user_input}}',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('extracts only used parameters for llm single run', () => {
|
||||
const parameters = ensureStartNodeParameters().map((item) => ({
|
||||
...item,
|
||||
name: 'start_1.user_input',
|
||||
ref: 'start_1.user_input',
|
||||
formLabel: '流程开始 > 用户问题',
|
||||
displayName: '流程开始 > 用户问题',
|
||||
systemReserved: false,
|
||||
autoManaged: true,
|
||||
}));
|
||||
const result = buildSingleRunParameters({
|
||||
type: 'llmNode',
|
||||
data: {
|
||||
userPrompt: '请回答 {{start_1.user_input}}',
|
||||
systemPrompt: '系统',
|
||||
parameters,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]?.formLabel).toBe('流程开始 > 用户问题');
|
||||
expect(result[0]?.required).toBe(true);
|
||||
});
|
||||
|
||||
it('builds field-mode single run model for llm node', () => {
|
||||
const parameters = ensureStartNodeParameters().map((item) => ({
|
||||
...item,
|
||||
name: 'start_1.user_input',
|
||||
ref: 'start_1.user_input',
|
||||
formLabel: '流程开始 > 用户问题',
|
||||
displayName: '流程开始 > 用户问题',
|
||||
systemReserved: false,
|
||||
autoManaged: true,
|
||||
}));
|
||||
|
||||
const result = buildSingleRunModel({
|
||||
type: 'llmNode',
|
||||
data: {
|
||||
userPrompt: '请回答 {{start_1.user_input}}',
|
||||
systemPrompt: '你是助手',
|
||||
parameters,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.mode).toBe('fields');
|
||||
expect(result.fields.map((item) => item.key)).toEqual([
|
||||
'systemPrompt',
|
||||
'userPrompt',
|
||||
]);
|
||||
expect(result.parameters).toHaveLength(1);
|
||||
expect(result.parameters[0]?.formLabel).toBe('流程开始 > 用户问题');
|
||||
});
|
||||
|
||||
it('keeps legacy start node parameters unchanged during single run build', () => {
|
||||
const legacyParameters = [
|
||||
{
|
||||
id: 'legacy_1',
|
||||
name: 'legacy_input',
|
||||
refType: 'input',
|
||||
dataType: 'String',
|
||||
},
|
||||
];
|
||||
|
||||
const result = buildSingleRunParameters({
|
||||
type: 'startNode',
|
||||
data: {
|
||||
parameters: legacyParameters,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual(legacyParameters);
|
||||
});
|
||||
|
||||
it('normalizes only start nodes that already contain fixed user_input', () => {
|
||||
const normalizedWorkflow = normalizeWorkflowStartNodes({
|
||||
nodes: [
|
||||
{
|
||||
id: 'start_new',
|
||||
type: 'startNode',
|
||||
data: {
|
||||
parameters: [
|
||||
{
|
||||
name: 'user_input',
|
||||
refType: 'input',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'start_legacy',
|
||||
type: 'startNode',
|
||||
data: {
|
||||
parameters: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
});
|
||||
|
||||
expect(normalizedWorkflow.nodes[0]?.data?.parameters?.[0]?.required).toBe(
|
||||
true,
|
||||
);
|
||||
expect(normalizedWorkflow.nodes[1]?.data?.parameters).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user