fix: 保持开始节点字段配置一致
- 同步字段重命名后的默认标签 - 保留图片等资源参数的内容类型
This commit is contained in:
@@ -49,10 +49,13 @@ describe('workflow node fields', () => {
|
||||
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: ['售前', '售后'],
|
||||
});
|
||||
const nextData = appendStartFormField(
|
||||
startNode.data as Record<string, any>,
|
||||
{
|
||||
type: 'select',
|
||||
options: ['售前', '售后'],
|
||||
},
|
||||
);
|
||||
|
||||
expect(nextData.startFormSchema).toHaveLength(2);
|
||||
expect(nextData.startFormSchema?.[1]).toMatchObject({
|
||||
@@ -71,54 +74,64 @@ describe('workflow node fields', () => {
|
||||
|
||||
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 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',
|
||||
contentType: 'file',
|
||||
placeholder: '请选择文件',
|
||||
});
|
||||
expect((updated.parameters as any[]).find((item) => item.name === 'file_field'))
|
||||
.toMatchObject({
|
||||
name: 'file_field',
|
||||
dataType: 'File',
|
||||
contentType: 'file',
|
||||
});
|
||||
expect(
|
||||
updated.startFormSchema?.find((item: any) => item.key === 'file_field'),
|
||||
).toMatchObject({
|
||||
key: 'file_field',
|
||||
label: '文件字段',
|
||||
type: 'file',
|
||||
contentType: '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 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',
|
||||
contentType: 'file',
|
||||
placeholder: '请选择文件',
|
||||
});
|
||||
expect((updated.parameters as any[]).find((item) => item.name === 'attachments'))
|
||||
.toMatchObject({
|
||||
dataType: 'File',
|
||||
contentType: 'file',
|
||||
});
|
||||
expect(
|
||||
updated.startFormSchema?.find((item: any) => item.key === 'attachments'),
|
||||
).toMatchObject({
|
||||
type: 'file',
|
||||
contentType: '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);
|
||||
@@ -129,10 +142,15 @@ describe('workflow node fields', () => {
|
||||
|
||||
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 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',
|
||||
);
|
||||
@@ -141,7 +159,9 @@ describe('workflow node fields', () => {
|
||||
key: 'topic',
|
||||
label: '主题',
|
||||
});
|
||||
const nextField = updated.startFormSchema?.find((item: any) => item.key === 'topic');
|
||||
const nextField = updated.startFormSchema?.find(
|
||||
(item: any) => item.key === 'topic',
|
||||
);
|
||||
const nextParameter = (updated.parameters as any[]).find(
|
||||
(item) => item.name === 'topic',
|
||||
);
|
||||
@@ -151,6 +171,161 @@ describe('workflow node fields', () => {
|
||||
expect(nextParameter?.id).toBe(previousParameter?.id);
|
||||
});
|
||||
|
||||
it('aligns the default field label when renaming a generated key', () => {
|
||||
const initial = createInitialWorkflowData();
|
||||
const appended = appendStartFormField(
|
||||
initial.nodes[0]?.data as Record<string, any>,
|
||||
{
|
||||
type: 'text',
|
||||
},
|
||||
);
|
||||
|
||||
const updated = updateStartFormField(appended, 'text_field', {
|
||||
key: '补充信息',
|
||||
});
|
||||
|
||||
expect(
|
||||
updated.startFormSchema?.find((item: any) => item.key === '补充信息'),
|
||||
).toMatchObject({
|
||||
key: '补充信息',
|
||||
label: '补充信息',
|
||||
});
|
||||
expect(
|
||||
(updated.parameters as any[]).find((item) => item.name === '补充信息'),
|
||||
).toMatchObject({
|
||||
name: '补充信息',
|
||||
formLabel: '补充信息',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps start fields unchanged when renaming to an existing key', () => {
|
||||
const initial = createInitialWorkflowData();
|
||||
const withTopic = appendStartFormField(
|
||||
initial.nodes[0]?.data as Record<string, any>,
|
||||
{
|
||||
key: 'topic',
|
||||
label: '主题',
|
||||
type: 'text',
|
||||
},
|
||||
);
|
||||
const withDetails = appendStartFormField(withTopic, {
|
||||
key: 'details',
|
||||
label: '详情',
|
||||
type: 'textarea',
|
||||
});
|
||||
const previousFields = withDetails.startFormSchema;
|
||||
const previousParameters = withDetails.parameters;
|
||||
|
||||
expect(
|
||||
isStartFormFieldKeyAvailable(withDetails, 'details', 'summary'),
|
||||
).toBe(true);
|
||||
expect(isStartFormFieldKeyAvailable(withDetails, 'details', 'topic')).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
isStartFormFieldKeyAvailable(withDetails, 'details', 'user_input'),
|
||||
).toBe(false);
|
||||
|
||||
const duplicatedCustomKey = updateStartFormField(withDetails, 'details', {
|
||||
key: 'topic',
|
||||
});
|
||||
const duplicatedSystemKey = updateStartFormField(withDetails, 'details', {
|
||||
key: 'user_input',
|
||||
});
|
||||
|
||||
expect(duplicatedCustomKey.startFormSchema).toEqual(previousFields);
|
||||
expect(duplicatedCustomKey.parameters).toEqual(previousParameters);
|
||||
expect(duplicatedSystemKey.startFormSchema).toEqual(previousFields);
|
||||
expect(duplicatedSystemKey.parameters).toEqual(previousParameters);
|
||||
});
|
||||
|
||||
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('preserves resource content type independently from input type', () => {
|
||||
for (const contentType of ['image', 'video', 'audio', 'other'] as const) {
|
||||
const key = `preview_${contentType}`;
|
||||
@@ -244,130 +419,6 @@ describe('workflow node fields', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps start fields unchanged when renaming to an existing key', () => {
|
||||
const initial = createInitialWorkflowData();
|
||||
const withTopic = appendStartFormField(
|
||||
initial.nodes[0]?.data as Record<string, any>,
|
||||
{
|
||||
key: 'topic',
|
||||
label: '主题',
|
||||
type: 'text',
|
||||
},
|
||||
);
|
||||
const withDetails = appendStartFormField(withTopic, {
|
||||
key: 'details',
|
||||
label: '详情',
|
||||
type: 'textarea',
|
||||
});
|
||||
const previousFields = withDetails.startFormSchema;
|
||||
const previousParameters = withDetails.parameters;
|
||||
|
||||
expect(
|
||||
isStartFormFieldKeyAvailable(withDetails, 'details', 'summary'),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isStartFormFieldKeyAvailable(withDetails, 'details', 'topic'),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isStartFormFieldKeyAvailable(withDetails, 'details', 'user_input'),
|
||||
).toBe(false);
|
||||
|
||||
const duplicatedCustomKey = updateStartFormField(withDetails, 'details', {
|
||||
key: 'topic',
|
||||
});
|
||||
const duplicatedSystemKey = updateStartFormField(withDetails, 'details', {
|
||||
key: 'user_input',
|
||||
});
|
||||
|
||||
expect(duplicatedCustomKey.startFormSchema).toEqual(previousFields);
|
||||
expect(duplicatedCustomKey.parameters).toEqual(previousParameters);
|
||||
expect(duplicatedSystemKey.startFormSchema).toEqual(previousFields);
|
||||
expect(duplicatedSystemKey.parameters).toEqual(previousParameters);
|
||||
});
|
||||
|
||||
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>,
|
||||
@@ -406,7 +457,9 @@ describe('workflow node fields', () => {
|
||||
edges,
|
||||
[],
|
||||
);
|
||||
const topicParameter = parameters.find((item) => item.name === 'start_1.topic_name');
|
||||
const topicParameter = parameters.find(
|
||||
(item) => item.name === 'start_1.topic_name',
|
||||
);
|
||||
|
||||
expect(topicParameter?.displayName).toBe('开始节点 > topic_name');
|
||||
expect(topicParameter?.formLabel).toBe('开始节点 > topic_name');
|
||||
@@ -491,8 +544,12 @@ describe('workflow node fields', () => {
|
||||
edges,
|
||||
[],
|
||||
);
|
||||
const documentsParameter = parameters.find((item) => item.name === 'knowledge_1.documents');
|
||||
const contentParameter = parameters.find((item) => item.name === 'knowledge_1.documents.content');
|
||||
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');
|
||||
@@ -558,12 +615,7 @@ describe('workflow node fields', () => {
|
||||
position: { x: 240, y: 120 },
|
||||
data: { title: '循环外模型', parameters: [] },
|
||||
};
|
||||
const nodes = [
|
||||
knowledgeNode,
|
||||
loopNode,
|
||||
insideNode,
|
||||
outsideNode,
|
||||
];
|
||||
const nodes = [knowledgeNode, loopNode, insideNode, outsideNode];
|
||||
|
||||
const insideParameters = buildEditorReferenceParameters(
|
||||
'llm_inside',
|
||||
@@ -591,13 +643,10 @@ describe('workflow node fields', () => {
|
||||
'loop_1.loopItem.content',
|
||||
]);
|
||||
expect(
|
||||
insideParameters.find(
|
||||
(item) => item.name === 'loop_1.loopItem.content',
|
||||
)?.dataType,
|
||||
insideParameters.find((item) => item.name === 'loop_1.loopItem.content')
|
||||
?.dataType,
|
||||
).toBe('String');
|
||||
expect(
|
||||
outsideParameters.map((item) => item.name),
|
||||
).toEqual(['loop_1.res']);
|
||||
expect(outsideParameters.map((item) => item.name)).toEqual(['loop_1.res']);
|
||||
expect(
|
||||
outsideParameters.some((item) =>
|
||||
['loop_1.index', 'loop_1.loopItem'].includes(item.name || ''),
|
||||
@@ -650,12 +699,17 @@ describe('workflow node fields', () => {
|
||||
[],
|
||||
);
|
||||
|
||||
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');
|
||||
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', () => {
|
||||
@@ -685,8 +739,9 @@ describe('workflow node fields', () => {
|
||||
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);
|
||||
expect(
|
||||
(patch?.[FIELD_BINDING_META_KEY] as any)?.userPrompt?.userModified,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('does not auto-bind from legacy start nodes without user_input', () => {
|
||||
@@ -821,7 +876,9 @@ describe('workflow node fields', () => {
|
||||
[startNode, knowledgeNode, llmNode],
|
||||
[],
|
||||
);
|
||||
expect((patch?.parameters as any[])?.[0]?.name).toBe('knowledge_1.documents');
|
||||
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');
|
||||
@@ -1138,9 +1195,7 @@ describe('workflow node fields', () => {
|
||||
expect(
|
||||
(normalizedWorkflow.nodes[0]?.data as Record<string, any> | undefined)
|
||||
?.startFormSchema?.[0]?.key,
|
||||
).toBe(
|
||||
'user_input',
|
||||
);
|
||||
).toBe('user_input');
|
||||
expect(normalizedWorkflow.nodes[1]?.data?.parameters).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user