fix: 修复工作流入口标题与提示词校验

- 支持系统入口标题清空占位与下游引用展示同步

- 降低字段编辑触发的节点图重算和无效渲染

- 在保存及执行前拦截空白的大模型用户提示词
This commit is contained in:
2026-08-10 23:15:41 +08:00
parent 6bfd440214
commit 1c0fbfa5ff
5 changed files with 482 additions and 58 deletions

View File

@@ -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}`;