fix: 修复工作流入口标题与提示词校验
- 支持系统入口标题清空占位与下游引用展示同步 - 降低字段编辑触发的节点图重算和无效渲染 - 在保存及执行前拦截空白的大模型用户提示词
This commit is contained in:
@@ -34,18 +34,57 @@ describe('workflow node fields', () => {
|
||||
expect(parameters[0]?.name).toBe('user_input');
|
||||
expect(parameters[0]?.systemReserved).toBe(true);
|
||||
expect(parameters[0]?.required).toBe(true);
|
||||
expect(parameters[0]?.formLabel).toBe('');
|
||||
expect(parameters[0]?.displayName).toBe('流程开始 > 用户问题');
|
||||
expect(initial.nodes[0]?.data?.startFormMeta).toMatchObject({
|
||||
title: '开始问答',
|
||||
submitText: '开始',
|
||||
});
|
||||
expect(initial.nodes[0]?.data?.startFormSchema?.[0]).toMatchObject({
|
||||
key: 'user_input',
|
||||
label: '',
|
||||
type: 'textarea',
|
||||
systemReserved: true,
|
||||
required: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves an empty system start title for the default placeholder', () => {
|
||||
const initial = createInitialWorkflowData();
|
||||
const startData = initial.nodes[0]?.data as Record<string, any>;
|
||||
const customized = updateStartFormField(startData, 'user_input', {
|
||||
label: '反洗钱',
|
||||
});
|
||||
const cleared = updateStartFormField(customized, 'user_input', {
|
||||
label: '',
|
||||
});
|
||||
|
||||
expect(customized.startFormSchema?.[0]?.label).toBe('反洗钱');
|
||||
expect(cleared.startFormSchema?.[0]?.label).toBe('');
|
||||
expect((cleared.parameters as any[])?.[0]?.formLabel).toBe('');
|
||||
expect((cleared.parameters as any[])?.[0]?.displayName).toBe(
|
||||
'流程开始 > 用户问题',
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves an explicitly entered default or prefixed system title', () => {
|
||||
const initial = createInitialWorkflowData();
|
||||
const startData = initial.nodes[0]?.data as Record<string, any>;
|
||||
const defaultTitle = updateStartFormField(startData, 'user_input', {
|
||||
label: '用户问题',
|
||||
});
|
||||
const prefixedTitle = updateStartFormField(startData, 'user_input', {
|
||||
label: '用户问题说明',
|
||||
});
|
||||
|
||||
expect(defaultTitle.startFormSchema?.[0]?.label).toBe('用户问题');
|
||||
expect((defaultTitle.parameters as any[])?.[0]?.formLabel).toBe('用户问题');
|
||||
expect(prefixedTitle.startFormSchema?.[0]?.label).toBe('用户问题说明');
|
||||
expect((prefixedTitle.parameters as any[])?.[0]?.formLabel).toBe(
|
||||
'用户问题说明',
|
||||
);
|
||||
});
|
||||
|
||||
it('appends custom start form field into schema source of truth', () => {
|
||||
const initial = createInitialWorkflowData();
|
||||
const startNode = initial.nodes[0]!;
|
||||
@@ -326,6 +365,129 @@ describe('workflow node fields', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('refreshes downstream reference labels when the system title changes', () => {
|
||||
const initial = createInitialWorkflowData();
|
||||
const customStartData = updateStartFormField(
|
||||
initial.nodes[0]?.data as Record<string, any>,
|
||||
'user_input',
|
||||
{ label: '反洗钱' },
|
||||
);
|
||||
const clearedStartData = updateStartFormField(
|
||||
customStartData,
|
||||
'user_input',
|
||||
{ label: '' },
|
||||
);
|
||||
const startNode: Node = {
|
||||
id: 'start_1',
|
||||
type: 'startNode',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
title: '开始节点',
|
||||
...clearedStartData,
|
||||
},
|
||||
};
|
||||
const llmNode: Node = {
|
||||
id: 'llm_1',
|
||||
type: 'llmNode',
|
||||
position: { x: 120, y: 0 },
|
||||
data: {
|
||||
title: '大模型',
|
||||
userPrompt: '',
|
||||
parameters: [
|
||||
{
|
||||
id: 'param_1',
|
||||
name: 'start_1.user_input',
|
||||
ref: 'start_1.user_input',
|
||||
refType: 'ref',
|
||||
autoManaged: true,
|
||||
formLabel: '开始节点 > 反洗钱',
|
||||
displayName: '开始节点 > 反洗钱',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const endNode: Node = {
|
||||
id: 'end_1',
|
||||
type: 'endNode',
|
||||
position: { x: 240, y: 0 },
|
||||
data: {
|
||||
title: '结束节点',
|
||||
parameters: [
|
||||
{
|
||||
id: 'param_2',
|
||||
name: 'start_1.user_input',
|
||||
ref: 'start_1.user_input',
|
||||
refType: 'ref',
|
||||
formLabel: '开始节点 > 反洗钱',
|
||||
displayName: '开始节点 > 反洗钱',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const edges: Edge[] = [
|
||||
{ id: 'edge_1', source: 'start_1', target: 'llm_1' } as Edge,
|
||||
{ id: 'edge_2', source: 'llm_1', target: 'end_1' } as Edge,
|
||||
];
|
||||
|
||||
const nextNodes = renameStartFieldReferencesInNodes(
|
||||
[startNode, llmNode, endNode],
|
||||
edges,
|
||||
'start_1',
|
||||
'user_input',
|
||||
'user_input',
|
||||
);
|
||||
const nextLlmNode = nextNodes.find((node) => node.id === 'llm_1')!;
|
||||
const nextParameter = (nextLlmNode.data?.parameters as any[])?.[0];
|
||||
const nextEndParameter = (
|
||||
nextNodes.find((node) => node.id === 'end_1')?.data?.parameters as any[]
|
||||
)?.[0];
|
||||
|
||||
expect(nextLlmNode.data?.userPrompt).toBe('');
|
||||
expect(nextParameter?.name).toBe('start_1.user_input');
|
||||
expect(nextParameter?.ref).toBe('start_1.user_input');
|
||||
expect(nextParameter?.displayName).toBe('开始节点 > 用户问题');
|
||||
expect(nextParameter?.formLabel).toBe('开始节点 > 用户问题');
|
||||
expect(nextEndParameter?.displayName).toBe('开始节点 > 用户问题');
|
||||
expect(nextEndParameter?.formLabel).toBe('开始节点 > 用户问题');
|
||||
expect((nextLlmNode.data as any)?.[FIELD_BINDING_META_KEY]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('preserves node identities when a title sync has nothing to update', () => {
|
||||
const initial = createInitialWorkflowData();
|
||||
const startNode: Node = {
|
||||
...initial.nodes[0]!,
|
||||
id: 'start_1',
|
||||
};
|
||||
const untouchedNode: Node = {
|
||||
id: 'code_1',
|
||||
type: 'codeNode',
|
||||
position: { x: 120, y: 0 },
|
||||
data: {
|
||||
title: '代码节点',
|
||||
parameters: [
|
||||
{
|
||||
id: 'param_1',
|
||||
name: 'manual_value',
|
||||
displayName: '手工参数',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const nodes = [startNode, untouchedNode];
|
||||
|
||||
const nextNodes = renameStartFieldReferencesInNodes(
|
||||
nodes,
|
||||
[],
|
||||
'start_1',
|
||||
'user_input',
|
||||
'user_input',
|
||||
);
|
||||
|
||||
expect(nextNodes).toBe(nodes);
|
||||
expect(nextNodes[0]).toBe(startNode);
|
||||
expect(nextNodes[1]).toBe(untouchedNode);
|
||||
});
|
||||
|
||||
it('preserves resource content type independently from input type', () => {
|
||||
for (const contentType of ['image', 'video', 'audio', 'other'] as const) {
|
||||
const key = `preview_${contentType}`;
|
||||
|
||||
Reference in New Issue
Block a user