1010 lines
27 KiB
TypeScript
1010 lines
27 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import type { Edge, Node } from '@xyflow/svelte';
|
|
|
|
import {
|
|
appendStartFormField,
|
|
buildAutoBindingPatch,
|
|
buildSequentialFieldBindingPatches,
|
|
buildFieldBindingPatch,
|
|
buildEditorReferenceParameters,
|
|
buildSingleRunModel,
|
|
buildSingleRunParameters,
|
|
createInitialWorkflowData,
|
|
ensureStartNodeParameters,
|
|
FIELD_BINDING_META_KEY,
|
|
normalizeStartNodeData,
|
|
normalizeWorkflowStartNodes,
|
|
renameStartFieldReferencesInNodes,
|
|
removeStartFormField,
|
|
updateStartFormField,
|
|
} 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);
|
|
expect(initial.nodes[0]?.data?.startFormMeta).toMatchObject({
|
|
title: '开始问答',
|
|
submitText: '开始',
|
|
});
|
|
expect(initial.nodes[0]?.data?.startFormSchema?.[0]).toMatchObject({
|
|
key: 'user_input',
|
|
type: 'textarea',
|
|
systemReserved: true,
|
|
required: true,
|
|
});
|
|
});
|
|
|
|
it('appends custom start form field into schema source of truth', () => {
|
|
const initial = createInitialWorkflowData();
|
|
const startNode = initial.nodes[0]!;
|
|
const nextData = appendStartFormField(startNode.data as Record<string, any>, {
|
|
type: 'select',
|
|
options: ['售前', '售后'],
|
|
});
|
|
|
|
expect(nextData.startFormSchema).toHaveLength(2);
|
|
expect(nextData.startFormSchema?.[1]).toMatchObject({
|
|
key: 'select_field',
|
|
label: '下拉字段',
|
|
type: 'select',
|
|
options: ['售前', '售后'],
|
|
});
|
|
expect((nextData.parameters as any[])?.[1]).toMatchObject({
|
|
name: 'select_field',
|
|
formLabel: '下拉字段',
|
|
formType: 'select',
|
|
enums: ['售前', '售后'],
|
|
});
|
|
});
|
|
|
|
it('updates generated field key when switching field type', () => {
|
|
const initial = createInitialWorkflowData();
|
|
const appended = appendStartFormField(initial.nodes[0]?.data as Record<string, any>, {
|
|
type: 'text',
|
|
});
|
|
|
|
const updated = updateStartFormField(appended, 'text_field', {
|
|
type: 'file',
|
|
placeholder: '请选择文件',
|
|
});
|
|
expect(updated.startFormSchema?.find((item: any) => item.key === 'file_field'))
|
|
.toMatchObject({
|
|
key: 'file_field',
|
|
label: '文件字段',
|
|
type: 'file',
|
|
placeholder: '请选择文件',
|
|
});
|
|
expect((updated.parameters as any[]).find((item) => item.name === 'file_field'))
|
|
.toMatchObject({
|
|
name: 'file_field',
|
|
dataType: 'File',
|
|
contentType: 'file',
|
|
});
|
|
});
|
|
|
|
it('updates and removes custom start form fields through schema source of truth', () => {
|
|
const initial = createInitialWorkflowData();
|
|
const appended = appendStartFormField(initial.nodes[0]?.data as Record<string, any>, {
|
|
key: 'attachments',
|
|
label: '附件',
|
|
type: 'text',
|
|
placeholder: '请输入内容',
|
|
});
|
|
|
|
const updated = updateStartFormField(appended, 'attachments', {
|
|
type: 'file',
|
|
placeholder: '请选择文件',
|
|
});
|
|
expect(updated.startFormSchema?.find((item: any) => item.key === 'attachments'))
|
|
.toMatchObject({
|
|
type: 'file',
|
|
placeholder: '请选择文件',
|
|
});
|
|
expect((updated.parameters as any[]).find((item) => item.name === 'attachments'))
|
|
.toMatchObject({
|
|
dataType: 'File',
|
|
contentType: 'file',
|
|
});
|
|
|
|
const removed = removeStartFormField(updated, 'attachments');
|
|
expect(removed.startFormSchema).toHaveLength(1);
|
|
expect((removed.parameters as any[]).map((item) => item.name)).toEqual([
|
|
'user_input',
|
|
]);
|
|
});
|
|
|
|
it('keeps custom start field parameter id stable when renaming key', () => {
|
|
const initial = createInitialWorkflowData();
|
|
const appended = appendStartFormField(initial.nodes[0]?.data as Record<string, any>, {
|
|
type: 'text',
|
|
});
|
|
const previousField = appended.startFormSchema?.find((item: any) => item.key === 'text_field');
|
|
const previousParameter = (appended.parameters as any[]).find(
|
|
(item) => item.name === 'text_field',
|
|
);
|
|
|
|
const updated = updateStartFormField(appended, 'text_field', {
|
|
key: 'topic',
|
|
label: '主题',
|
|
});
|
|
const nextField = updated.startFormSchema?.find((item: any) => item.key === 'topic');
|
|
const nextParameter = (updated.parameters as any[]).find(
|
|
(item) => item.name === 'topic',
|
|
);
|
|
|
|
expect(previousField?.id).toBeTruthy();
|
|
expect(nextField?.id).toBe(previousField?.id);
|
|
expect(nextParameter?.id).toBe(previousParameter?.id);
|
|
});
|
|
|
|
it('renames downstream token and managed references when start field key changes', () => {
|
|
const initialStartData = appendStartFormField(
|
|
createInitialWorkflowData().nodes[0]?.data as Record<string, any>,
|
|
{
|
|
type: 'text',
|
|
},
|
|
);
|
|
const startNode: Node = {
|
|
id: 'start_1',
|
|
type: 'startNode',
|
|
position: { x: 0, y: 0 },
|
|
data: {
|
|
title: '开始节点',
|
|
...initialStartData,
|
|
},
|
|
};
|
|
const renamedStartData = updateStartFormField(
|
|
startNode.data as Record<string, any>,
|
|
'text_field',
|
|
{
|
|
key: 'topic',
|
|
label: '主题',
|
|
},
|
|
);
|
|
const llmNode: Node = {
|
|
id: 'llm_1',
|
|
type: 'llmNode',
|
|
position: { x: 120, y: 0 },
|
|
data: {
|
|
title: '大模型',
|
|
userPrompt: '请围绕 {{start_1.text_field}} 生成内容',
|
|
parameters: [
|
|
{
|
|
id: 'param_1',
|
|
name: 'start_1.text_field',
|
|
ref: 'start_1.text_field',
|
|
refType: 'ref',
|
|
autoManaged: true,
|
|
formLabel: '开始节点 > 文本字段',
|
|
displayName: '开始节点 > 文本字段',
|
|
},
|
|
],
|
|
[FIELD_BINDING_META_KEY]: {
|
|
userPrompt: {
|
|
autoFilledFrom: 'start_1.text_field',
|
|
userModified: false,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const edges: Edge[] = [
|
|
{ id: 'edge_1', source: 'start_1', target: 'llm_1' } as Edge,
|
|
];
|
|
|
|
const nextNodes = renameStartFieldReferencesInNodes(
|
|
[
|
|
{
|
|
...startNode,
|
|
data: {
|
|
...(startNode.data as Record<string, any>),
|
|
...renamedStartData,
|
|
},
|
|
},
|
|
llmNode,
|
|
],
|
|
edges,
|
|
'start_1',
|
|
'text_field',
|
|
'topic',
|
|
);
|
|
const nextLlmNode = nextNodes.find((node) => node.id === 'llm_1')!;
|
|
const nextParameter = (nextLlmNode.data?.parameters as any[])?.[0];
|
|
|
|
expect(nextLlmNode.data?.userPrompt).toBe('请围绕 {{start_1.topic}} 生成内容');
|
|
expect(nextParameter?.name).toBe('start_1.topic');
|
|
expect(nextParameter?.ref).toBe('start_1.topic');
|
|
expect(nextParameter?.displayName).toBe('开始节点 > topic');
|
|
expect((nextLlmNode.data as any)?.[FIELD_BINDING_META_KEY]?.userPrompt).toMatchObject({
|
|
autoFilledFrom: 'start_1.topic',
|
|
userModified: false,
|
|
});
|
|
});
|
|
|
|
it('uses custom start parameter name for reference display', () => {
|
|
const startData = appendStartFormField(
|
|
createInitialWorkflowData().nodes[0]?.data as Record<string, any>,
|
|
{
|
|
key: 'topic_name',
|
|
label: '下拉字段',
|
|
type: 'select',
|
|
options: ['A', 'B'],
|
|
},
|
|
);
|
|
const startNode: Node = {
|
|
id: 'start_1',
|
|
type: 'startNode',
|
|
position: { x: 0, y: 0 },
|
|
data: {
|
|
title: '开始节点',
|
|
...startData,
|
|
},
|
|
};
|
|
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,
|
|
[],
|
|
);
|
|
const topicParameter = parameters.find((item) => item.name === 'start_1.topic_name');
|
|
|
|
expect(topicParameter?.displayName).toBe('开始节点 > topic_name');
|
|
expect(topicParameter?.formLabel).toBe('开始节点 > topic_name');
|
|
});
|
|
|
|
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('uses output parameter name for reference display', () => {
|
|
const knowledgeNode: Node = {
|
|
id: 'knowledge_1',
|
|
type: 'knowledgeNode',
|
|
position: { x: 0, y: 0 },
|
|
data: {
|
|
title: '知识库',
|
|
outputDefs: [
|
|
{
|
|
name: 'documents',
|
|
formLabel: '文档列表',
|
|
children: [
|
|
{
|
|
name: 'content',
|
|
formLabel: '正文内容',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
const llmNode: Node = {
|
|
id: 'llm_1',
|
|
type: 'llmNode',
|
|
position: { x: 120, y: 0 },
|
|
data: {
|
|
title: '大模型',
|
|
parameters: [],
|
|
},
|
|
};
|
|
const edges: Edge[] = [
|
|
{ id: 'edge_1', source: 'knowledge_1', target: 'llm_1' } as Edge,
|
|
];
|
|
|
|
const parameters = buildEditorReferenceParameters(
|
|
'llm_1',
|
|
[knowledgeNode, llmNode],
|
|
edges,
|
|
[],
|
|
);
|
|
const documentsParameter = parameters.find((item) => item.name === 'knowledge_1.documents');
|
|
const contentParameter = parameters.find((item) => item.name === 'knowledge_1.documents.content');
|
|
|
|
expect(documentsParameter?.displayName).toBe('知识库 > documents');
|
|
expect(documentsParameter?.formLabel).toBe('知识库 > documents');
|
|
expect(documentsParameter?.pathLabel).toBe('documents');
|
|
expect(documentsParameter?.isCollection).toBe(true);
|
|
expect(contentParameter?.displayName).toBe('知识库 > documents.[].content');
|
|
expect(contentParameter?.formLabel).toBe('知识库 > documents.[].content');
|
|
expect(contentParameter?.pathLabel).toBe('documents.[].content');
|
|
expect(contentParameter?.itemTypeLabel).toBe('数组项字段');
|
|
});
|
|
|
|
it('exposes typed loop variables only to nodes inside the loop body', () => {
|
|
const knowledgeNode: Node = {
|
|
id: 'knowledge_1',
|
|
type: 'knowledgeNode',
|
|
position: { x: 0, y: 0 },
|
|
data: {
|
|
title: '知识库',
|
|
outputDefs: [
|
|
{
|
|
name: 'documents',
|
|
dataType: 'Array',
|
|
children: [
|
|
{ name: 'title', dataType: 'String' },
|
|
{ name: 'content', dataType: 'String' },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
const loopNode: Node = {
|
|
id: 'loop_1',
|
|
type: 'loopNode',
|
|
position: { x: 120, y: 0 },
|
|
data: {
|
|
title: '循环',
|
|
loopInputs: {
|
|
items: {
|
|
name: 'items',
|
|
refType: 'ref',
|
|
ref: 'knowledge_1.documents',
|
|
dataType: 'Array',
|
|
},
|
|
},
|
|
outputDefs: [
|
|
{
|
|
name: 'res',
|
|
dataType: 'Array<String>',
|
|
},
|
|
],
|
|
},
|
|
};
|
|
const insideNode: Node = {
|
|
id: 'llm_inside',
|
|
type: 'llmNode',
|
|
parentId: 'loop_1',
|
|
position: { x: 240, y: 0 },
|
|
data: { title: '循环内模型', parameters: [] },
|
|
};
|
|
const outsideNode: Node = {
|
|
id: 'llm_outside',
|
|
type: 'llmNode',
|
|
position: { x: 240, y: 120 },
|
|
data: { title: '循环外模型', parameters: [] },
|
|
};
|
|
const nodes = [
|
|
knowledgeNode,
|
|
loopNode,
|
|
insideNode,
|
|
outsideNode,
|
|
];
|
|
|
|
const insideParameters = buildEditorReferenceParameters(
|
|
'llm_inside',
|
|
nodes,
|
|
[],
|
|
[],
|
|
);
|
|
const outsideParameters = buildEditorReferenceParameters(
|
|
'llm_outside',
|
|
nodes,
|
|
[
|
|
{
|
|
id: 'loop_to_outside',
|
|
source: 'loop_1',
|
|
target: 'llm_outside',
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
|
|
expect(insideParameters.map((item) => item.name)).toEqual([
|
|
'loop_1.index',
|
|
'loop_1.loopItem',
|
|
'loop_1.loopItem.title',
|
|
'loop_1.loopItem.content',
|
|
]);
|
|
expect(
|
|
insideParameters.find(
|
|
(item) => item.name === 'loop_1.loopItem.content',
|
|
)?.dataType,
|
|
).toBe('String');
|
|
expect(
|
|
outsideParameters.map((item) => item.name),
|
|
).toEqual(['loop_1.res']);
|
|
expect(
|
|
outsideParameters.some((item) =>
|
|
['loop_1.index', 'loop_1.loopItem'].includes(item.name || ''),
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('uses document node child outputs for reference display', () => {
|
|
const documentNode: Node = {
|
|
id: 'doc_1',
|
|
type: 'document-node',
|
|
position: { x: 0, y: 0 },
|
|
data: {
|
|
title: '文档解析',
|
|
outputDefs: [
|
|
{
|
|
name: 'documents',
|
|
formLabel: '文档列表',
|
|
children: [
|
|
{
|
|
name: 'fileName',
|
|
formLabel: '文件名',
|
|
},
|
|
{
|
|
name: 'content',
|
|
formLabel: '正文内容',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
const llmNode: Node = {
|
|
id: 'llm_1',
|
|
type: 'llmNode',
|
|
position: { x: 120, y: 0 },
|
|
data: {
|
|
title: '大模型',
|
|
parameters: [],
|
|
},
|
|
};
|
|
const edges: Edge[] = [
|
|
{ id: 'edge_1', source: 'doc_1', target: 'llm_1' } as Edge,
|
|
];
|
|
|
|
const parameters = buildEditorReferenceParameters(
|
|
'llm_1',
|
|
[documentNode, llmNode],
|
|
edges,
|
|
[],
|
|
);
|
|
|
|
expect(parameters.find((item) => item.name === 'doc_1.documents')?.displayName)
|
|
.toBe('文档解析 > documents');
|
|
expect(parameters.find((item) => item.name === 'doc_1.documents.fileName')?.displayName)
|
|
.toBe('文档解析 > documents.[].fileName');
|
|
expect(parameters.find((item) => item.name === 'doc_1.documents.content')?.displayName)
|
|
.toBe('文档解析 > documents.[].content');
|
|
});
|
|
|
|
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 start node schema from parameters', () => {
|
|
const normalized = normalizeStartNodeData({
|
|
parameters: [
|
|
{
|
|
id: 'system_1',
|
|
name: 'user_input',
|
|
refType: 'input',
|
|
required: true,
|
|
formType: 'input',
|
|
formLabel: '问题',
|
|
formPlaceholder: '请输入问题',
|
|
},
|
|
{
|
|
id: 'file_1',
|
|
name: 'attachments',
|
|
refType: 'input',
|
|
dataType: 'File',
|
|
contentType: 'file',
|
|
formLabel: '附件',
|
|
required: false,
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(normalized.startFormSchema).toHaveLength(2);
|
|
expect(normalized.startFormSchema[0]).toMatchObject({
|
|
key: 'user_input',
|
|
type: 'text',
|
|
systemReserved: true,
|
|
required: true,
|
|
});
|
|
expect(normalized.startFormSchema[1]).toMatchObject({
|
|
key: 'attachments',
|
|
type: 'file',
|
|
required: false,
|
|
});
|
|
expect(normalized.parameters[1]).toMatchObject({
|
|
name: 'attachments',
|
|
contentType: 'file',
|
|
dataType: 'File',
|
|
});
|
|
});
|
|
|
|
it('forces invalid user_input schema back to required text input', () => {
|
|
const normalized = normalizeStartNodeData({
|
|
startFormSchema: [
|
|
{
|
|
key: 'user_input',
|
|
label: '主问题',
|
|
type: 'radio',
|
|
required: false,
|
|
options: ['A', 'B'],
|
|
},
|
|
],
|
|
startFormMeta: {
|
|
title: '',
|
|
description: '',
|
|
submitText: '',
|
|
},
|
|
});
|
|
|
|
expect(normalized.startFormSchema[0]).toMatchObject({
|
|
key: 'user_input',
|
|
type: 'textarea',
|
|
required: true,
|
|
systemReserved: true,
|
|
});
|
|
expect(normalized.startFormMeta).toMatchObject({
|
|
title: '',
|
|
description: '',
|
|
submitText: '开始',
|
|
});
|
|
expect(normalized.parameters[0]).toMatchObject({
|
|
name: 'user_input',
|
|
formType: 'textarea',
|
|
required: true,
|
|
contentType: 'text',
|
|
});
|
|
});
|
|
|
|
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[0]?.data as Record<string, any> | undefined)
|
|
?.startFormSchema?.[0]?.key,
|
|
).toBe(
|
|
'user_input',
|
|
);
|
|
expect(normalizedWorkflow.nodes[1]?.data?.parameters).toEqual([]);
|
|
});
|
|
});
|