发布 v1.10 #5

Merged
czm merged 147 commits from develop into main 2026-08-20 11:36:27 +08:00
2 changed files with 440 additions and 290 deletions
Showing only changes of commit c757eeed50 - Show all commits

View File

@@ -49,10 +49,13 @@ describe('workflow node fields', () => {
it('appends custom start form field into schema source of truth', () => { it('appends custom start form field into schema source of truth', () => {
const initial = createInitialWorkflowData(); const initial = createInitialWorkflowData();
const startNode = initial.nodes[0]!; const startNode = initial.nodes[0]!;
const nextData = appendStartFormField(startNode.data as Record<string, any>, { const nextData = appendStartFormField(
startNode.data as Record<string, any>,
{
type: 'select', type: 'select',
options: ['售前', '售后'], options: ['售前', '售后'],
}); },
);
expect(nextData.startFormSchema).toHaveLength(2); expect(nextData.startFormSchema).toHaveLength(2);
expect(nextData.startFormSchema?.[1]).toMatchObject({ expect(nextData.startFormSchema?.[1]).toMatchObject({
@@ -71,24 +74,29 @@ describe('workflow node fields', () => {
it('updates generated field key when switching field type', () => { it('updates generated field key when switching field type', () => {
const initial = createInitialWorkflowData(); const initial = createInitialWorkflowData();
const appended = appendStartFormField(initial.nodes[0]?.data as Record<string, any>, { const appended = appendStartFormField(
initial.nodes[0]?.data as Record<string, any>,
{
type: 'text', type: 'text',
}); },
);
const updated = updateStartFormField(appended, 'text_field', { const updated = updateStartFormField(appended, 'text_field', {
type: 'file', type: 'file',
placeholder: '请选择文件', placeholder: '请选择文件',
}); });
expect(updated.startFormSchema?.find((item: any) => item.key === 'file_field')) expect(
.toMatchObject({ updated.startFormSchema?.find((item: any) => item.key === 'file_field'),
).toMatchObject({
key: 'file_field', key: 'file_field',
label: '文件字段', label: '文件字段',
type: 'file', type: 'file',
contentType: 'file', contentType: 'file',
placeholder: '请选择文件', placeholder: '请选择文件',
}); });
expect((updated.parameters as any[]).find((item) => item.name === 'file_field')) expect(
.toMatchObject({ (updated.parameters as any[]).find((item) => item.name === 'file_field'),
).toMatchObject({
name: 'file_field', name: 'file_field',
dataType: 'File', dataType: 'File',
contentType: 'file', contentType: 'file',
@@ -97,25 +105,30 @@ describe('workflow node fields', () => {
it('updates and removes custom start form fields through schema source of truth', () => { it('updates and removes custom start form fields through schema source of truth', () => {
const initial = createInitialWorkflowData(); const initial = createInitialWorkflowData();
const appended = appendStartFormField(initial.nodes[0]?.data as Record<string, any>, { const appended = appendStartFormField(
initial.nodes[0]?.data as Record<string, any>,
{
key: 'attachments', key: 'attachments',
label: '附件', label: '附件',
type: 'text', type: 'text',
placeholder: '请输入内容', placeholder: '请输入内容',
}); },
);
const updated = updateStartFormField(appended, 'attachments', { const updated = updateStartFormField(appended, 'attachments', {
type: 'file', type: 'file',
placeholder: '请选择文件', placeholder: '请选择文件',
}); });
expect(updated.startFormSchema?.find((item: any) => item.key === 'attachments')) expect(
.toMatchObject({ updated.startFormSchema?.find((item: any) => item.key === 'attachments'),
).toMatchObject({
type: 'file', type: 'file',
contentType: 'file', contentType: 'file',
placeholder: '请选择文件', placeholder: '请选择文件',
}); });
expect((updated.parameters as any[]).find((item) => item.name === 'attachments')) expect(
.toMatchObject({ (updated.parameters as any[]).find((item) => item.name === 'attachments'),
).toMatchObject({
dataType: 'File', dataType: 'File',
contentType: 'file', contentType: 'file',
}); });
@@ -129,10 +142,15 @@ describe('workflow node fields', () => {
it('keeps custom start field parameter id stable when renaming key', () => { it('keeps custom start field parameter id stable when renaming key', () => {
const initial = createInitialWorkflowData(); const initial = createInitialWorkflowData();
const appended = appendStartFormField(initial.nodes[0]?.data as Record<string, any>, { const appended = appendStartFormField(
initial.nodes[0]?.data as Record<string, any>,
{
type: 'text', type: 'text',
}); },
const previousField = appended.startFormSchema?.find((item: any) => item.key === 'text_field'); );
const previousField = appended.startFormSchema?.find(
(item: any) => item.key === 'text_field',
);
const previousParameter = (appended.parameters as any[]).find( const previousParameter = (appended.parameters as any[]).find(
(item) => item.name === 'text_field', (item) => item.name === 'text_field',
); );
@@ -141,7 +159,9 @@ describe('workflow node fields', () => {
key: 'topic', key: 'topic',
label: '主题', 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( const nextParameter = (updated.parameters as any[]).find(
(item) => item.name === 'topic', (item) => item.name === 'topic',
); );
@@ -151,6 +171,161 @@ describe('workflow node fields', () => {
expect(nextParameter?.id).toBe(previousParameter?.id); 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', () => { it('preserves resource content type independently from input type', () => {
for (const contentType of ['image', 'video', 'audio', 'other'] as const) { for (const contentType of ['image', 'video', 'audio', 'other'] as const) {
const key = `preview_${contentType}`; 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', () => { it('uses custom start parameter name for reference display', () => {
const startData = appendStartFormField( const startData = appendStartFormField(
createInitialWorkflowData().nodes[0]?.data as Record<string, any>, createInitialWorkflowData().nodes[0]?.data as Record<string, any>,
@@ -406,7 +457,9 @@ describe('workflow node fields', () => {
edges, 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?.displayName).toBe('开始节点 > topic_name');
expect(topicParameter?.formLabel).toBe('开始节点 > topic_name'); expect(topicParameter?.formLabel).toBe('开始节点 > topic_name');
@@ -491,8 +544,12 @@ describe('workflow node fields', () => {
edges, edges,
[], [],
); );
const documentsParameter = parameters.find((item) => item.name === 'knowledge_1.documents'); const documentsParameter = parameters.find(
const contentParameter = parameters.find((item) => item.name === 'knowledge_1.documents.content'); (item) => item.name === 'knowledge_1.documents',
);
const contentParameter = parameters.find(
(item) => item.name === 'knowledge_1.documents.content',
);
expect(documentsParameter?.displayName).toBe('知识库 > documents'); expect(documentsParameter?.displayName).toBe('知识库 > documents');
expect(documentsParameter?.formLabel).toBe('知识库 > documents'); expect(documentsParameter?.formLabel).toBe('知识库 > documents');
@@ -558,12 +615,7 @@ describe('workflow node fields', () => {
position: { x: 240, y: 120 }, position: { x: 240, y: 120 },
data: { title: '循环外模型', parameters: [] }, data: { title: '循环外模型', parameters: [] },
}; };
const nodes = [ const nodes = [knowledgeNode, loopNode, insideNode, outsideNode];
knowledgeNode,
loopNode,
insideNode,
outsideNode,
];
const insideParameters = buildEditorReferenceParameters( const insideParameters = buildEditorReferenceParameters(
'llm_inside', 'llm_inside',
@@ -591,13 +643,10 @@ describe('workflow node fields', () => {
'loop_1.loopItem.content', 'loop_1.loopItem.content',
]); ]);
expect( expect(
insideParameters.find( insideParameters.find((item) => item.name === 'loop_1.loopItem.content')
(item) => item.name === 'loop_1.loopItem.content', ?.dataType,
)?.dataType,
).toBe('String'); ).toBe('String');
expect( expect(outsideParameters.map((item) => item.name)).toEqual(['loop_1.res']);
outsideParameters.map((item) => item.name),
).toEqual(['loop_1.res']);
expect( expect(
outsideParameters.some((item) => outsideParameters.some((item) =>
['loop_1.index', 'loop_1.loopItem'].includes(item.name || ''), ['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) expect(
.toBe('文档解析 > documents'); parameters.find((item) => item.name === 'doc_1.documents')?.displayName,
expect(parameters.find((item) => item.name === 'doc_1.documents.fileName')?.displayName) ).toBe('文档解析 > documents');
.toBe('文档解析 > documents.[].fileName'); expect(
expect(parameters.find((item) => item.name === 'doc_1.documents.content')?.displayName) parameters.find((item) => item.name === 'doc_1.documents.fileName')
.toBe('文档解析 > documents.[].content'); ?.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', () => { 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); const patch = buildAutoBindingPatch(llmNode, [startNode, llmNode], edges);
expect(patch?.userPrompt).toBe('{{start_1.user_input}}'); expect(patch?.userPrompt).toBe('{{start_1.user_input}}');
expect((patch?.parameters as any[])?.[0]?.name).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) expect(
.toBe(false); (patch?.[FIELD_BINDING_META_KEY] as any)?.userPrompt?.userModified,
).toBe(false);
}); });
it('does not auto-bind from legacy start nodes without user_input', () => { it('does not auto-bind from legacy start nodes without user_input', () => {
@@ -821,7 +876,9 @@ describe('workflow node fields', () => {
[startNode, knowledgeNode, llmNode], [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]?.disconnected).toBe(true);
expect((patch?.parameters as any[])?.[0]?.displayName).toBe('documents'); expect((patch?.parameters as any[])?.[0]?.displayName).toBe('documents');
expect(patch).not.toHaveProperty('systemPrompt'); expect(patch).not.toHaveProperty('systemPrompt');
@@ -1138,9 +1195,7 @@ describe('workflow node fields', () => {
expect( expect(
(normalizedWorkflow.nodes[0]?.data as Record<string, any> | undefined) (normalizedWorkflow.nodes[0]?.data as Record<string, any> | undefined)
?.startFormSchema?.[0]?.key, ?.startFormSchema?.[0]?.key,
).toBe( ).toBe('user_input');
'user_input',
);
expect(normalizedWorkflow.nodes[1]?.data?.parameters).toEqual([]); expect(normalizedWorkflow.nodes[1]?.data?.parameters).toEqual([]);
}); });
}); });

View File

@@ -95,15 +95,19 @@ const OPTION_FIELD_TYPE_SET = new Set<StartFormFieldType>([
const DEFAULT_START_FORM_TITLE = '开始问答'; const DEFAULT_START_FORM_TITLE = '开始问答';
const DEFAULT_START_FORM_DESCRIPTION = '请先补充必要信息,再开始执行工作流。'; const DEFAULT_START_FORM_DESCRIPTION = '请先补充必要信息,再开始执行工作流。';
const DEFAULT_START_FORM_SUBMIT_TEXT = '开始'; const DEFAULT_START_FORM_SUBMIT_TEXT = '开始';
const START_FORM_DEFAULT_FIELD_KEY_BY_TYPE: Record<StartFormFieldType, string> = { const START_FORM_DEFAULT_FIELD_KEY_BY_TYPE: Record<StartFormFieldType, string> =
{
text: 'text_field', text: 'text_field',
textarea: 'textarea_field', textarea: 'textarea_field',
radio: 'radio_field', radio: 'radio_field',
checkbox: 'checkbox_field', checkbox: 'checkbox_field',
select: 'select_field', select: 'select_field',
file: 'file_field', file: 'file_field',
}; };
const START_FORM_DEFAULT_FIELD_LABEL_BY_TYPE: Record<StartFormFieldType, string> = { const START_FORM_DEFAULT_FIELD_LABEL_BY_TYPE: Record<
StartFormFieldType,
string
> = {
text: '文本字段', text: '文本字段',
textarea: '长文本字段', textarea: '长文本字段',
radio: '单选字段', radio: '单选字段',
@@ -135,12 +139,18 @@ function ensureStringArray(value: unknown) {
return value.map((item) => asString(item).trim()).filter(Boolean); return value.map((item) => asString(item).trim()).filter(Boolean);
} }
function isSupportedStartFormFieldType(value: unknown): value is StartFormFieldType { function isSupportedStartFormFieldType(
return START_FORM_FIELD_TYPE_SET.has(asString(value).trim() as StartFormFieldType); value: unknown,
): value is StartFormFieldType {
return START_FORM_FIELD_TYPE_SET.has(
asString(value).trim() as StartFormFieldType,
);
} }
function isOptionFieldType(value: unknown): value is StartFormFieldType { function isOptionFieldType(value: unknown): value is StartFormFieldType {
return OPTION_FIELD_TYPE_SET.has(asString(value).trim() as StartFormFieldType); return OPTION_FIELD_TYPE_SET.has(
asString(value).trim() as StartFormFieldType,
);
} }
function normalizeStartFormContentType( function normalizeStartFormContentType(
@@ -193,10 +203,7 @@ function isCollectionOutputParameter(parameter?: Parameter | null) {
const dataType = asString(parameter?.dataType).trim(); const dataType = asString(parameter?.dataType).trim();
return ( return (
!!parameter?.children?.length && !!parameter?.children?.length &&
( (dataType === 'Array' || dataType.length === 0)
dataType === 'Array' ||
dataType.length === 0
)
); );
} }
@@ -271,7 +278,11 @@ function flattenOutputDefs(
const path = parentPath ? `${parentPath}.${rawName}` : rawName; const path = parentPath ? `${parentPath}.${rawName}` : rawName;
const referenceLabel = getReferenceParameterLabel(parameter); const referenceLabel = getReferenceParameterLabel(parameter);
const label = buildCollectionAwareLabel(referenceLabel, parentLabel, parentIsCollection); const label = buildCollectionAwareLabel(
referenceLabel,
parentLabel,
parentIsCollection,
);
const displayName = `${getNodeTitle(node)} > ${label}`; const displayName = `${getNodeTitle(node)} > ${label}`;
const isCollection = isCollectionOutputParameter(parameter); const isCollection = isCollectionOutputParameter(parameter);
const fullRef = `${node.id}.${path}`; const fullRef = `${node.id}.${path}`;
@@ -376,10 +387,15 @@ function uniqueParameters(parameters: Parameter[]) {
} }
function findParameterByName(parameters: Parameter[], name: string) { function findParameterByName(parameters: Parameter[], name: string) {
return parameters.find((parameter) => asString(parameter.name).trim() === name); return parameters.find(
(parameter) => asString(parameter.name).trim() === name,
);
} }
function toManagedRefParameter(refPath: string, candidate?: Parameter): Parameter { function toManagedRefParameter(
refPath: string,
candidate?: Parameter,
): Parameter {
return ensureParameterId({ return ensureParameterId({
name: refPath, name: refPath,
ref: candidate?.ref || refPath, ref: candidate?.ref || refPath,
@@ -448,7 +464,10 @@ function isAutoGeneratedStartFormFieldKey(key: unknown) {
return true; return true;
} }
return Object.values(START_FORM_DEFAULT_FIELD_KEY_BY_TYPE).some((baseKey) => { return Object.values(START_FORM_DEFAULT_FIELD_KEY_BY_TYPE).some((baseKey) => {
return normalizedKey === baseKey || new RegExp(`^${baseKey}_[0-9]+$`).test(normalizedKey); return (
normalizedKey === baseKey ||
new RegExp(`^${baseKey}_[0-9]+$`).test(normalizedKey)
);
}); });
} }
@@ -460,14 +479,18 @@ function isDefaultStartFormFieldLabel(label: unknown) {
if (normalizedLabel === '新字段') { if (normalizedLabel === '新字段') {
return true; return true;
} }
return Object.values(START_FORM_DEFAULT_FIELD_LABEL_BY_TYPE).includes(normalizedLabel); return Object.values(START_FORM_DEFAULT_FIELD_LABEL_BY_TYPE).includes(
normalizedLabel,
);
} }
function ensureStartFormFieldId( function ensureStartFormFieldId(
fieldId: unknown, fieldId: unknown,
existingParameter?: Parameter | null, existingParameter?: Parameter | null,
) { ) {
return trimString(fieldId) || trimString(existingParameter?.id) || genShortId(); return (
trimString(fieldId) || trimString(existingParameter?.id) || genShortId()
);
} }
function createDefaultStartFormMeta(): StartFormMeta { function createDefaultStartFormMeta(): StartFormMeta {
@@ -478,17 +501,24 @@ function createDefaultStartFormMeta(): StartFormMeta {
}; };
} }
export function normalizeStartFormMeta(meta?: Partial<StartFormMeta> | null): StartFormMeta { export function normalizeStartFormMeta(
meta?: Partial<StartFormMeta> | null,
): StartFormMeta {
const fallback = createDefaultStartFormMeta(); const fallback = createDefaultStartFormMeta();
return { return {
// Missing values use defaults; an explicit empty string means the user cleared the field. // Missing values use defaults; an explicit empty string means the user cleared the field.
title: meta?.title == null ? fallback.title : trimString(meta.title), title: meta?.title == null ? fallback.title : trimString(meta.title),
description: meta?.description == null ? fallback.description : trimString(meta.description), description:
meta?.description == null
? fallback.description
: trimString(meta.description),
submitText: trimString(meta?.submitText) || fallback.submitText, submitText: trimString(meta?.submitText) || fallback.submitText,
}; };
} }
function normalizeSystemStartParameter(parameter?: Parameter | null): Parameter { function normalizeSystemStartParameter(
parameter?: Parameter | null,
): Parameter {
const fallbackLabel = SYSTEM_START_PARAM_LABEL; const fallbackLabel = SYSTEM_START_PARAM_LABEL;
const formLabel = trimString(parameter?.formLabel) || fallbackLabel; const formLabel = trimString(parameter?.formLabel) || fallbackLabel;
const formType = normalizeSystemStartFormType(parameter?.formType); const formType = normalizeSystemStartFormType(parameter?.formType);
@@ -513,7 +543,9 @@ function normalizeSystemStartParameter(parameter?: Parameter | null): Parameter
}); });
} }
function parameterTypeToStartFormFieldType(parameter?: Parameter | null): StartFormFieldType { function parameterTypeToStartFormFieldType(
parameter?: Parameter | null,
): StartFormFieldType {
if ( if (
trimString(parameter?.contentType) === 'file' || trimString(parameter?.contentType) === 'file' ||
trimString(parameter?.dataType).toLowerCase() === 'file' trimString(parameter?.dataType).toLowerCase() === 'file'
@@ -605,6 +637,17 @@ function normalizeStartFormField(
const options = isOptionFieldType(type) const options = isOptionFieldType(type)
? ensureStringArray(field?.options || existingParameter?.enums) ? ensureStringArray(field?.options || existingParameter?.enums)
: []; : [];
const requestedLabel =
trimString(field?.label) ||
trimString(existingParameter?.formLabel) ||
trimString(existingParameter?.displayName) ||
key;
const label =
!isSystemField &&
isDefaultStartFormFieldLabel(requestedLabel) &&
!isAutoGeneratedStartFormFieldKey(key)
? key
: requestedLabel;
const defaultValue = Array.isArray(field?.defaultValue) const defaultValue = Array.isArray(field?.defaultValue)
? ensureStringArray(field?.defaultValue) ? ensureStringArray(field?.defaultValue)
@@ -613,18 +656,18 @@ function normalizeStartFormField(
return { return {
id: ensureStartFormFieldId(field?.id, existingParameter), id: ensureStartFormFieldId(field?.id, existingParameter),
key, key,
label: label,
trimString(field?.label) ||
trimString(existingParameter?.formLabel) ||
trimString(existingParameter?.displayName) ||
key,
type, type,
contentType, contentType,
required: isSystemField ? true : Boolean(field?.required ?? existingParameter?.required), required: isSystemField
? true
: Boolean(field?.required ?? existingParameter?.required),
placeholder: placeholder:
trimString(field?.placeholder) || trimString(existingParameter?.formPlaceholder), trimString(field?.placeholder) ||
trimString(existingParameter?.formPlaceholder),
description: description:
trimString(field?.description) || trimString(existingParameter?.formDescription), trimString(field?.description) ||
trimString(existingParameter?.formDescription),
defaultValue, defaultValue,
options, options,
systemReserved: isSystemField, systemReserved: isSystemField,
@@ -684,7 +727,9 @@ function startFormFieldToParameter(
defaultValue: Array.isArray(field.defaultValue) defaultValue: Array.isArray(field.defaultValue)
? field.defaultValue.join('\n') ? field.defaultValue.join('\n')
: asString(field.defaultValue), : asString(field.defaultValue),
enums: isOptionFieldType(field.type) ? ensureStringArray(field.options) : [], enums: isOptionFieldType(field.type)
? ensureStringArray(field.options)
: [],
displayName: `流程开始 > ${formLabel}`, displayName: `流程开始 > ${formLabel}`,
systemReserved: false, systemReserved: false,
}); });
@@ -708,7 +753,8 @@ export function normalizeStartFormSchema(
.filter(([key]) => key.length > 0), .filter(([key]) => key.length > 0),
); );
const source = Array.isArray(schema) && schema.length > 0 const source =
Array.isArray(schema) && schema.length > 0
? schema ? schema
: parameterCandidates : parameterCandidates
.map((parameter) => parameterToStartFormField(parameter)) .map((parameter) => parameterToStartFormField(parameter))
@@ -727,7 +773,8 @@ export function normalizeStartFormSchema(
} }
const normalized = normalizeStartFormField( const normalized = normalizeStartFormField(
item as Partial<StartFormFieldSchema>, item as Partial<StartFormFieldSchema>,
(fieldId ? parameterIdMap.get(fieldId) : undefined) || parameterMap.get(key), (fieldId ? parameterIdMap.get(fieldId) : undefined) ||
parameterMap.get(key),
); );
if (!normalized) { if (!normalized) {
continue; continue;
@@ -749,7 +796,8 @@ export function normalizeStartFormSchema(
{ {
id: trimString(systemParameter?.id), id: trimString(systemParameter?.id),
key: SYSTEM_START_PARAM_NAME, key: SYSTEM_START_PARAM_NAME,
label: trimString(systemParameter?.formLabel) || SYSTEM_START_PARAM_LABEL, label:
trimString(systemParameter?.formLabel) || SYSTEM_START_PARAM_LABEL,
type: type:
normalizeSystemStartFormType(systemParameter?.formType) === 'input' normalizeSystemStartFormType(systemParameter?.formType) === 'input'
? 'text' ? 'text'
@@ -810,7 +858,8 @@ export function createCustomStartFormField(
requestedType === 'file' ? 'file' : 'text', requestedType === 'file' ? 'file' : 'text',
); );
const type = contentType === 'file' ? 'file' : requestedType; const type = contentType === 'file' ? 'file' : requestedType;
const key = trimString(field?.key) || buildUniqueStartFormFieldKey(type, existingKeys); const key =
trimString(field?.key) || buildUniqueStartFormFieldKey(type, existingKeys);
return { return {
id: ensureStartFormFieldId(field?.id), id: ensureStartFormFieldId(field?.id),
key, key,
@@ -838,7 +887,10 @@ export function appendStartFormField(
const currentParameters = Array.isArray(currentData.parameters) const currentParameters = Array.isArray(currentData.parameters)
? (currentData.parameters as Parameter[]) ? (currentData.parameters as Parameter[])
: []; : [];
const schema = normalizeStartFormSchema(currentData.startFormSchema, currentParameters); const schema = normalizeStartFormSchema(
currentData.startFormSchema,
currentParameters,
);
const nextField = createCustomStartFormField( const nextField = createCustomStartFormField(
field, field,
schema.map((item) => item.key), schema.map((item) => item.key),
@@ -858,7 +910,10 @@ export function updateStartFormField(
const currentParameters = Array.isArray(currentData.parameters) const currentParameters = Array.isArray(currentData.parameters)
? (currentData.parameters as Parameter[]) ? (currentData.parameters as Parameter[])
: []; : [];
const schema = normalizeStartFormSchema(currentData.startFormSchema, currentParameters); const schema = normalizeStartFormSchema(
currentData.startFormSchema,
currentParameters,
);
if ( if (
patch.key !== undefined && patch.key !== undefined &&
!isStartFormFieldKeyAvailable(currentData, currentKey, patch.key) !isStartFormFieldKeyAvailable(currentData, currentKey, patch.key)
@@ -881,19 +936,21 @@ export function updateStartFormField(
return { return {
...field, ...field,
...patch, ...patch,
key: trimString(patch.key) || ( key:
shouldAutoRenameKey trimString(patch.key) ||
(shouldAutoRenameKey
? buildUniqueStartFormFieldKey( ? buildUniqueStartFormFieldKey(
nextType, nextType,
schema schema
.filter((item) => item.key !== field.key) .filter((item) => item.key !== field.key)
.map((item) => item.key), .map((item) => item.key),
) )
: field.key : field.key),
), label:
label: trimString(patch.label) || ( trimString(patch.label) ||
shouldAutoRenameLabel ? getDefaultStartFormFieldLabel(nextType) : field.label (shouldAutoRenameLabel
), ? getDefaultStartFormFieldLabel(nextType)
: field.label),
type: nextType, type: nextType,
}; };
}); });
@@ -931,7 +988,10 @@ export function removeStartFormField(
const currentParameters = Array.isArray(currentData.parameters) const currentParameters = Array.isArray(currentData.parameters)
? (currentData.parameters as Parameter[]) ? (currentData.parameters as Parameter[])
: []; : [];
const schema = normalizeStartFormSchema(currentData.startFormSchema, currentParameters); const schema = normalizeStartFormSchema(
currentData.startFormSchema,
currentParameters,
);
return normalizeStartNodeData({ return normalizeStartNodeData({
...currentData, ...currentData,
startFormSchema: schema.filter((field) => field.key !== currentKey), startFormSchema: schema.filter((field) => field.key !== currentKey),
@@ -959,7 +1019,10 @@ export function normalizeStartNodeData(
}; };
} }
const schema = normalizeStartFormSchema(currentData.startFormSchema, currentParameters); const schema = normalizeStartFormSchema(
currentData.startFormSchema,
currentParameters,
);
const parameterMap = new Map( const parameterMap = new Map(
currentParameters currentParameters
.map((parameter) => [trimString(parameter.name), parameter] as const) .map((parameter) => [trimString(parameter.name), parameter] as const)
@@ -973,8 +1036,9 @@ export function normalizeStartNodeData(
const parameters = schema.map((field) => const parameters = schema.map((field) =>
startFormFieldToParameter( startFormFieldToParameter(
field, field,
(trimString(field.id) ? parameterIdMap.get(trimString(field.id)) : undefined) || (trimString(field.id)
parameterMap.get(field.key), ? parameterIdMap.get(trimString(field.id))
: undefined) || parameterMap.get(field.key),
), ),
); );
@@ -1010,7 +1074,9 @@ export function ensureStartNodeParameters(parameters?: Parameter[]) {
const source = Array.isArray(parameters) const source = Array.isArray(parameters)
? parameters.map(cloneParameter) ? parameters.map(cloneParameter)
: []; : [];
const index = source.findIndex((parameter) => isSystemStartParameter(parameter)); const index = source.findIndex((parameter) =>
isSystemStartParameter(parameter),
);
if (index >= 0) { if (index >= 0) {
source[index] = normalizeSystemStartParameter(source[index]!); source[index] = normalizeSystemStartParameter(source[index]!);
@@ -1022,7 +1088,10 @@ export function ensureStartNodeParameters(parameters?: Parameter[]) {
.filter((parameter) => !isSystemStartParameter(parameter)) .filter((parameter) => !isSystemStartParameter(parameter))
.map((parameter) => ensureParameterId(parameter)); .map((parameter) => ensureParameterId(parameter));
return [source.find((parameter) => isSystemStartParameter(parameter))!, ...customParameters]; return [
source.find((parameter) => isSystemStartParameter(parameter))!,
...customParameters,
];
} }
export function createInitialWorkflowData() { export function createInitialWorkflowData() {
@@ -1055,7 +1124,9 @@ export function createInitialWorkflowData() {
}; };
} }
export function normalizeWorkflowStartNodes<T extends Record<string, any>>(data: T): T { export function normalizeWorkflowStartNodes<T extends Record<string, any>>(
data: T,
): T {
if (!data || typeof data !== 'object' || !Array.isArray(data.nodes)) { if (!data || typeof data !== 'object' || !Array.isArray(data.nodes)) {
return data; return data;
} }
@@ -1131,9 +1202,7 @@ export function buildEditorReferenceParameters(
buildLoopScopeParameters(node, currentNode, nodes).length, buildLoopScopeParameters(node, currentNode, nodes).length,
); );
}) })
.flatMap((node) => .flatMap((node) => getNodeReferenceParameters(node, currentNode, nodes));
getNodeReferenceParameters(node, currentNode, nodes),
);
const upstreamNameSet = new Set( const upstreamNameSet = new Set(
upstreamParameters.map((parameter) => asString(parameter.name).trim()), upstreamParameters.map((parameter) => asString(parameter.name).trim()),
@@ -1172,7 +1241,9 @@ export function syncManagedParametersForFields(
const tokenKeys = Array.from( const tokenKeys = Array.from(
new Set( new Set(
Object.values(fieldValues) Object.values(fieldValues)
.flatMap((value) => getTokenRanges(asString(value)).map((token) => token.key)) .flatMap((value) =>
getTokenRanges(asString(value)).map((token) => token.key),
)
.filter((value) => value.trim().length > 0), .filter((value) => value.trim().length > 0),
), ),
); );
@@ -1188,7 +1259,9 @@ export function syncManagedParametersForFields(
return true; return true;
} }
const parameterName = asString(parameter.name).trim(); const parameterName = asString(parameter.name).trim();
return usedTokenKeySet.has(parameterName) && candidateKeySet.has(parameterName); return (
usedTokenKeySet.has(parameterName) && candidateKeySet.has(parameterName)
);
}) })
.map((parameter) => { .map((parameter) => {
if (parameter.autoManaged !== true) { if (parameter.autoManaged !== true) {
@@ -1276,7 +1349,7 @@ function resolveNearestStartNodeRef(
} }
if (sourceNode.type === START_NODE_TYPE) { if (sourceNode.type === START_NODE_TYPE) {
const parameters = Array.isArray(sourceNode.data?.parameters) const parameters = Array.isArray(sourceNode.data?.parameters)
? ((sourceNode.data?.parameters as Parameter[]) || []) ? (sourceNode.data?.parameters as Parameter[]) || []
: []; : [];
const systemParam = parameters.find((parameter) => const systemParam = parameters.find((parameter) =>
isSystemStartParameter(parameter), isSystemStartParameter(parameter),
@@ -1341,7 +1414,11 @@ function replaceStartFieldReferenceValue(
if (Array.isArray(value)) { if (Array.isArray(value)) {
let changed = false; let changed = false;
const nextValue = value.map((item) => { const nextValue = value.map((item) => {
const nextItem = replaceStartFieldReferenceValue(item, oldRefPath, newRefPath); const nextItem = replaceStartFieldReferenceValue(
item,
oldRefPath,
newRefPath,
);
if (nextItem !== item) { if (nextItem !== item) {
changed = true; changed = true;
} }
@@ -1371,10 +1448,7 @@ function replaceStartFieldReferenceValue(
return value; return value;
} }
export function collectDownstreamNodeIds( export function collectDownstreamNodeIds(rootNodeId: string, edges: Edge[]) {
rootNodeId: string,
edges: Edge[],
) {
const nodeIds = new Set<string>(); const nodeIds = new Set<string>();
const visit = (nodeId: string) => { const visit = (nodeId: string) => {
@@ -1383,7 +1457,9 @@ export function collectDownstreamNodeIds(
} }
nodeIds.add(nodeId); nodeIds.add(nodeId);
edges edges
.filter((edge) => edge.source === nodeId && edge.sourceHandle !== 'loop_handle') .filter(
(edge) => edge.source === nodeId && edge.sourceHandle !== 'loop_handle',
)
.forEach((edge) => { .forEach((edge) => {
if (edge.target) { if (edge.target) {
visit(edge.target); visit(edge.target);
@@ -1436,7 +1512,10 @@ export function renameStartFieldReferencesInNodes(
}; };
}); });
const affectedNodeIds = collectDownstreamNodeIds(normalizedStartNodeId, edges); const affectedNodeIds = collectDownstreamNodeIds(
normalizedStartNodeId,
edges,
);
if (affectedNodeIds.length === 0) { if (affectedNodeIds.length === 0) {
return nextNodes; return nextNodes;
} }
@@ -1450,7 +1529,9 @@ export function renameStartFieldReferencesInNodes(
return nextNodes; return nextNodes;
} }
const patchMap = new Map(patches.map((item) => [item.nodeId, item.patch] as const)); const patchMap = new Map(
patches.map((item) => [item.nodeId, item.patch] as const),
);
return nextNodes.map((node) => { return nextNodes.map((node) => {
const patch = patchMap.get(node.id); const patch = patchMap.get(node.id);
if (!patch) { if (!patch) {
@@ -1539,7 +1620,8 @@ export function buildFieldBindingPatch(
const data = (node.data || {}) as Record<string, any>; const data = (node.data || {}) as Record<string, any>;
const startRef = resolveNearestStartNodeRef(node.id, nodes, edges); const startRef = resolveNearestStartNodeRef(node.id, nodes, edges);
const patch: Record<string, any> = {}; const patch: Record<string, any> = {};
const nextFieldValues: Record<string, string | undefined> = Object.fromEntries( const nextFieldValues: Record<string, string | undefined> =
Object.fromEntries(
fieldKeys.map((fieldKey) => [fieldKey, asString(data[fieldKey])]), fieldKeys.map((fieldKey) => [fieldKey, asString(data[fieldKey])]),
); );
const nextMeta: FieldBindingMeta = { const nextMeta: FieldBindingMeta = {
@@ -1551,7 +1633,13 @@ export function buildFieldBindingPatch(
return; return;
} }
const fieldMeta = nextMeta[fieldName]; const fieldMeta = nextMeta[fieldName];
if (!canApplyAutoBinding(nextFieldValues[fieldName], fieldMeta, startRef.refPath)) { if (
!canApplyAutoBinding(
nextFieldValues[fieldName],
fieldMeta,
startRef.refPath,
)
) {
return; return;
} }
const tokenValue = toToken(startRef.refPath); const tokenValue = toToken(startRef.refPath);
@@ -1565,7 +1653,10 @@ export function buildFieldBindingPatch(
}; };
}; };
const clearField = (fieldName: string, fieldMeta: NonNullable<FieldBindingMeta[string]>) => { const clearField = (
fieldName: string,
fieldMeta: NonNullable<FieldBindingMeta[string]>,
) => {
const currentValue = asString(nextFieldValues[fieldName]).trim(); const currentValue = asString(nextFieldValues[fieldName]).trim();
const autoTokenValue = toToken(fieldMeta.autoFilledFrom || ''); const autoTokenValue = toToken(fieldMeta.autoFilledFrom || '');
if (currentValue && currentValue !== autoTokenValue) { if (currentValue && currentValue !== autoTokenValue) {
@@ -1606,14 +1697,14 @@ export function buildFieldBindingPatch(
); );
patch[FIELD_BINDING_META_KEY] = nextMeta; patch[FIELD_BINDING_META_KEY] = nextMeta;
const currentParameters = JSON.stringify((data.parameters as Parameter[]) || []); const currentParameters = JSON.stringify(
(data.parameters as Parameter[]) || [],
);
if (JSON.stringify(patch.parameters) === currentParameters) { if (JSON.stringify(patch.parameters) === currentParameters) {
delete patch.parameters; delete patch.parameters;
} }
if ( if (JSON.stringify(nextMeta) === JSON.stringify(getFieldBindingMeta(data))) {
JSON.stringify(nextMeta) === JSON.stringify(getFieldBindingMeta(data))
) {
delete patch[FIELD_BINDING_META_KEY]; delete patch[FIELD_BINDING_META_KEY];
} }
@@ -1675,7 +1766,9 @@ export function buildSequentialFieldBindingPatches(
return patches; return patches;
} }
export function buildSingleRunParameters(node: Pick<Node, 'type' | 'data'> | null | undefined) { export function buildSingleRunParameters(
node: Pick<Node, 'type' | 'data'> | null | undefined,
) {
if (!node?.data) { if (!node?.data) {
return []; return [];
} }
@@ -1710,16 +1803,18 @@ export function buildSingleRunParameters(node: Pick<Node, 'type' | 'data'> | nul
const usedTokenKeys = Array.from( const usedTokenKeys = Array.from(
new Set( new Set(
fieldKeys.flatMap((fieldKey) => fieldKeys.flatMap((fieldKey) =>
getTokenRanges(asString((node.data as Record<string, any>)[fieldKey])).map( getTokenRanges(
(token) => token.key, asString((node.data as Record<string, any>)[fieldKey]),
), ).map((token) => token.key),
), ),
), ),
); );
return parameters.filter((parameter) => return parameters
.filter((parameter) =>
usedTokenKeys.includes(asString(parameter.name).trim()), usedTokenKeys.includes(asString(parameter.name).trim()),
).map((parameter) => )
.map((parameter) =>
ensureParameterId({ ensureParameterId({
...parameter, ...parameter,
required: parameter.required ?? true, required: parameter.required ?? true,