feat: 优化工作流字段化参数配置

- 开始节点固定 user_input 并区分系统入口与自定义参数

- LLM 与知识库节点切换为字段值加上游引用配置

- 单节点调试改为字段预览与上游引用输入模式
This commit is contained in:
2026-04-12 20:31:02 +08:00
parent 47655a728b
commit 8cfe5400fe
24 changed files with 2785 additions and 792 deletions

View File

@@ -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([]);
});
});