发布 v1.10 #5
@@ -832,11 +832,36 @@ public class WorkflowCheckService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (workflowDatacenterContentService.isLlmNode(node.type)) {
|
if (workflowDatacenterContentService.isLlmNode(node.type)) {
|
||||||
|
checkLlmUserPrompt(node, issues, issueKeys);
|
||||||
checkLlmQueryContext(node, parsed, issues, issueKeys);
|
checkLlmQueryContext(node, parsed, issues, issueKeys);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验大模型节点的用户提示词,避免空提示词进入运行时解析。
|
||||||
|
*
|
||||||
|
* @param node 大模型节点
|
||||||
|
* @param issues 问题列表
|
||||||
|
* @param issueKeys 问题去重键
|
||||||
|
*/
|
||||||
|
private void checkLlmUserPrompt(
|
||||||
|
NodeView node,
|
||||||
|
List<WorkflowCheckIssue> issues,
|
||||||
|
Set<String> issueKeys) {
|
||||||
|
String userPrompt = node.data == null ? null : node.data.getString("userPrompt");
|
||||||
|
if (!StringUtils.hasText(userPrompt)) {
|
||||||
|
addIssue(
|
||||||
|
issues,
|
||||||
|
issueKeys,
|
||||||
|
"LLM_USER_PROMPT_EMPTY",
|
||||||
|
"大模型节点的用户提示词不能为空",
|
||||||
|
node.id,
|
||||||
|
null,
|
||||||
|
node.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void checkMakeFileNode(NodeView node,
|
private void checkMakeFileNode(NodeView node,
|
||||||
List<WorkflowCheckIssue> issues,
|
List<WorkflowCheckIssue> issues,
|
||||||
Set<String> issueKeys) {
|
Set<String> issueKeys) {
|
||||||
|
|||||||
@@ -436,6 +436,41 @@ public class WorkflowCheckServiceTest {
|
|||||||
assertHasCode(result, "SEARCH_DATASET_INVALID");
|
assertHasCode(result, "SEARCH_DATASET_INVALID");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证保存阶段拒绝空白的大模型用户提示词。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testSaveShouldBlockBlankLlmUserPrompt() throws Exception {
|
||||||
|
WorkflowCheckService service = newService(new HashMap<>());
|
||||||
|
JSONObject llmData = data("大模型");
|
||||||
|
llmData.put("userPrompt", " ");
|
||||||
|
String content = workflowJson(
|
||||||
|
array(node("llm-1", "llmNode", null, llmData)),
|
||||||
|
new JSONArray()
|
||||||
|
);
|
||||||
|
|
||||||
|
WorkflowCheckResult result = service.checkContent(content, WorkflowCheckStage.SAVE, null);
|
||||||
|
Assert.assertFalse(result.isPassed());
|
||||||
|
assertHasCode(result, "LLM_USER_PROMPT_EMPTY");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证保存阶段接受有效的大模型用户提示词。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testSaveShouldPassNonBlankLlmUserPrompt() throws Exception {
|
||||||
|
WorkflowCheckService service = newService(new HashMap<>());
|
||||||
|
JSONObject llmData = data("大模型");
|
||||||
|
llmData.put("userPrompt", "{{start-1.user_input}}");
|
||||||
|
String content = workflowJson(
|
||||||
|
array(node("llm-1", "llmNode", null, llmData)),
|
||||||
|
new JSONArray()
|
||||||
|
);
|
||||||
|
|
||||||
|
WorkflowCheckResult result = service.checkContent(content, WorkflowCheckStage.SAVE, null);
|
||||||
|
Assert.assertTrue(result.isPassed());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPreExecuteShouldBlockMissingStartOrEnd() throws Exception {
|
public void testPreExecuteShouldBlockMissingStartOrEnd() throws Exception {
|
||||||
WorkflowCheckService service = newService(new HashMap<>());
|
WorkflowCheckService service = newService(new HashMap<>());
|
||||||
@@ -453,6 +488,34 @@ public class WorkflowCheckServiceTest {
|
|||||||
assertHasCode(result, "END_NODE_MISSING");
|
assertHasCode(result, "END_NODE_MISSING");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证执行前校验拒绝空的大模型用户提示词。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testPreExecuteShouldBlockEmptyLlmUserPrompt() throws Exception {
|
||||||
|
WorkflowCheckService service = newService(new HashMap<>());
|
||||||
|
JSONObject llmData = data("大模型");
|
||||||
|
llmData.put("userPrompt", "");
|
||||||
|
String content = workflowJson(
|
||||||
|
array(
|
||||||
|
node("s1", "startNode", null, data("开始")),
|
||||||
|
node("llm-1", "llmNode", null, llmData),
|
||||||
|
node("e1", "endNode", null, data("结束"))
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
edge("edge-1", "s1", "llm-1"),
|
||||||
|
edge("edge-2", "llm-1", "e1")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
WorkflowCheckResult result = service.checkContent(
|
||||||
|
content,
|
||||||
|
WorkflowCheckStage.PRE_EXECUTE,
|
||||||
|
BigInteger.ONE);
|
||||||
|
Assert.assertFalse(result.isPassed());
|
||||||
|
assertHasCode(result, "LLM_USER_PROMPT_EMPTY");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPreExecuteShouldPassForSourceOnlySearchDatasetNode() throws Exception {
|
public void testPreExecuteShouldPassForSourceOnlySearchDatasetNode() throws Exception {
|
||||||
WorkflowCheckService service = newService(new HashMap<>());
|
WorkflowCheckService service = newService(new HashMap<>());
|
||||||
|
|||||||
@@ -52,9 +52,10 @@
|
|||||||
return param.formType ? [param.formType] : [];
|
return param.formType ? [param.formType] : [];
|
||||||
});
|
});
|
||||||
let parameterNameDraft = $state<string | null>(null);
|
let parameterNameDraft = $state<string | null>(null);
|
||||||
|
let systemLabelDraft = $state<string | null>(null);
|
||||||
let displayParamName = $derived.by(() => {
|
let displayParamName = $derived.by(() => {
|
||||||
if (isSystemStartParam) {
|
if (isSystemStartParam) {
|
||||||
return '用户问题';
|
return systemLabelDraft ?? trimString(param.formLabel);
|
||||||
}
|
}
|
||||||
return parameterNameDraft ?? param.name;
|
return parameterNameDraft ?? param.name;
|
||||||
});
|
});
|
||||||
@@ -76,6 +77,8 @@
|
|||||||
|
|
||||||
const applyStartFieldPatch = (fieldKey: string, patch: Record<string, any>) => {
|
const applyStartFieldPatch = (fieldKey: string, patch: Record<string, any>) => {
|
||||||
const currentFieldId = trimString(param.id);
|
const currentFieldId = trimString(param.id);
|
||||||
|
const shouldSyncReferences = Object.prototype.hasOwnProperty.call(patch, 'key')
|
||||||
|
|| Object.prototype.hasOwnProperty.call(patch, 'label');
|
||||||
store.updateNodes((nodes) => {
|
store.updateNodes((nodes) => {
|
||||||
const edges = store.getEdges();
|
const edges = store.getEdges();
|
||||||
let nextFieldKey = fieldKey;
|
let nextFieldKey = fieldKey;
|
||||||
@@ -98,6 +101,9 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
if (!shouldSyncReferences) {
|
||||||
|
return nextNodes;
|
||||||
|
}
|
||||||
return renameStartFieldReferencesInNodes(
|
return renameStartFieldReferencesInNodes(
|
||||||
nextNodes,
|
nextNodes,
|
||||||
edges,
|
edges,
|
||||||
@@ -172,11 +178,11 @@
|
|||||||
const updateName = (event: Event) => {
|
const updateName = (event: Event) => {
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
const newValue = input.value;
|
const newValue = input.value;
|
||||||
|
parameterNameDraft = newValue;
|
||||||
if (isStartNodeInputParam) {
|
if (isStartNodeInputParam) {
|
||||||
const normalizedValue = trimString(newValue);
|
const normalizedValue = trimString(newValue);
|
||||||
if (!normalizedValue) {
|
if (!normalizedValue) {
|
||||||
parameterNameError = '参数名不能为空';
|
parameterNameError = '参数名不能为空';
|
||||||
parameterNameDraft = newValue;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isStartFormFieldKeyAvailable(
|
if (!isStartFormFieldKeyAvailable(
|
||||||
@@ -185,13 +191,66 @@
|
|||||||
normalizedValue
|
normalizedValue
|
||||||
)) {
|
)) {
|
||||||
parameterNameError = '参数名已存在';
|
parameterNameError = '参数名已存在';
|
||||||
parameterNameDraft = newValue;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
parameterNameError = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateVisibleName = (event: Event) => {
|
||||||
|
if (isSystemStartParam) {
|
||||||
|
systemLabelDraft = (event.target as HTMLInputElement).value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateName(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitName = () => {
|
||||||
|
if (parameterNameDraft === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const draftValue = parameterNameDraft;
|
||||||
|
if (isStartNodeInputParam) {
|
||||||
|
const normalizedValue = trimString(draftValue);
|
||||||
|
if (!normalizedValue) {
|
||||||
|
parameterNameError = '参数名不能为空';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isStartFormFieldKeyAvailable(
|
||||||
|
node?.current?.data as Record<string, any>,
|
||||||
|
param.name || '',
|
||||||
|
normalizedValue
|
||||||
|
)) {
|
||||||
|
parameterNameError = '参数名已存在';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parameterNameDraft = null;
|
||||||
|
parameterNameError = '';
|
||||||
|
if (normalizedValue !== trimString(param.name)) {
|
||||||
|
updateParameter('name', normalizedValue);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
parameterNameDraft = null;
|
parameterNameDraft = null;
|
||||||
parameterNameError = '';
|
parameterNameError = '';
|
||||||
updateParameter('name', newValue);
|
if (draftValue !== (param.name || '')) {
|
||||||
|
updateParameter('name', draftValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitVisibleName = () => {
|
||||||
|
if (!isSystemStartParam) {
|
||||||
|
commitName();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (systemLabelDraft === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextLabel = trimString(systemLabelDraft);
|
||||||
|
systemLabelDraft = null;
|
||||||
|
if (nextLabel !== trimString(param.formLabel)) {
|
||||||
|
updateParameter('formLabel', nextLabel);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateRequired = (event: Event) => {
|
const updateRequired = (event: Event) => {
|
||||||
@@ -236,11 +295,13 @@
|
|||||||
|
|
||||||
|
|
||||||
<div class="input-item input-item-name">
|
<div class="input-item input-item-name">
|
||||||
<Input style="width: 100%;" value={displayParamName} placeholder="请输入参数名称"
|
<Input style="width: 100%;" value={displayParamName}
|
||||||
disabled={param.nameDisabled === true}
|
placeholder={isSystemStartParam ? '用户问题' : '请输入参数名称'}
|
||||||
|
disabled={isSystemStartParam ? false : param.nameDisabled === true}
|
||||||
aria-invalid={parameterNameError ? 'true' : undefined}
|
aria-invalid={parameterNameError ? 'true' : undefined}
|
||||||
aria-describedby={parameterNameError ? parameterNameErrorId : undefined}
|
aria-describedby={parameterNameError ? parameterNameErrorId : undefined}
|
||||||
oninput={updateName} />
|
oninput={updateVisibleName}
|
||||||
|
onchange={commitVisibleName} />
|
||||||
{#if parameterNameError}
|
{#if parameterNameError}
|
||||||
<div id={parameterNameErrorId} class="input-error" role="alert">
|
<div id={parameterNameErrorId} class="input-error" role="alert">
|
||||||
{parameterNameError}
|
{parameterNameError}
|
||||||
@@ -296,7 +357,7 @@
|
|||||||
数据标题:
|
数据标题:
|
||||||
<Textarea rows={1} style="width: 100%;" onchange={(event)=>{
|
<Textarea rows={1} style="width: 100%;" onchange={(event)=>{
|
||||||
updateParamByEvent('formLabel', event)
|
updateParamByEvent('formLabel', event)
|
||||||
}} value={param.formLabel} />
|
}} value={param.formLabel} placeholder={isSystemStartParam ? '用户问题' : undefined} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-more-item">
|
<div class="input-more-item">
|
||||||
|
|||||||
@@ -34,18 +34,57 @@ describe('workflow node fields', () => {
|
|||||||
expect(parameters[0]?.name).toBe('user_input');
|
expect(parameters[0]?.name).toBe('user_input');
|
||||||
expect(parameters[0]?.systemReserved).toBe(true);
|
expect(parameters[0]?.systemReserved).toBe(true);
|
||||||
expect(parameters[0]?.required).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({
|
expect(initial.nodes[0]?.data?.startFormMeta).toMatchObject({
|
||||||
title: '开始问答',
|
title: '开始问答',
|
||||||
submitText: '开始',
|
submitText: '开始',
|
||||||
});
|
});
|
||||||
expect(initial.nodes[0]?.data?.startFormSchema?.[0]).toMatchObject({
|
expect(initial.nodes[0]?.data?.startFormSchema?.[0]).toMatchObject({
|
||||||
key: 'user_input',
|
key: 'user_input',
|
||||||
|
label: '',
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
systemReserved: true,
|
systemReserved: true,
|
||||||
required: 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', () => {
|
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]!;
|
||||||
@@ -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', () => {
|
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}`;
|
||||||
|
|||||||
@@ -184,6 +184,9 @@ function getNodeTitle(node?: Node | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getParameterLabel(parameter?: Parameter | null) {
|
function getParameterLabel(parameter?: Parameter | null) {
|
||||||
|
if (isSystemStartParameter(parameter)) {
|
||||||
|
return trimString(parameter?.formLabel) || SYSTEM_START_PARAM_LABEL;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
asString(parameter?.formLabel).trim() ||
|
asString(parameter?.formLabel).trim() ||
|
||||||
asString(parameter?.displayName).trim() ||
|
asString(parameter?.displayName).trim() ||
|
||||||
@@ -415,6 +418,14 @@ function normalizeSystemStartFormType(value: unknown): 'input' | 'textarea' {
|
|||||||
return trimString(value) === 'input' ? 'input' : 'textarea';
|
return trimString(value) === 'input' ? 'input' : 'textarea';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSystemStartFormLabel(value: unknown) {
|
||||||
|
return trimString(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSystemStartDisplayLabel(value: unknown) {
|
||||||
|
return normalizeSystemStartFormLabel(value) || SYSTEM_START_PARAM_LABEL;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeStartFormFieldUiType(
|
function normalizeStartFormFieldUiType(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
fallback: StartFormFieldType = 'text',
|
fallback: StartFormFieldType = 'text',
|
||||||
@@ -519,8 +530,8 @@ export function normalizeStartFormMeta(
|
|||||||
function normalizeSystemStartParameter(
|
function normalizeSystemStartParameter(
|
||||||
parameter?: Parameter | null,
|
parameter?: Parameter | null,
|
||||||
): Parameter {
|
): Parameter {
|
||||||
const fallbackLabel = SYSTEM_START_PARAM_LABEL;
|
const formLabel = normalizeSystemStartFormLabel(parameter?.formLabel);
|
||||||
const formLabel = trimString(parameter?.formLabel) || fallbackLabel;
|
const displayLabel = getSystemStartDisplayLabel(formLabel);
|
||||||
const formType = normalizeSystemStartFormType(parameter?.formType);
|
const formType = normalizeSystemStartFormType(parameter?.formType);
|
||||||
return ensureParameterId({
|
return ensureParameterId({
|
||||||
...cloneParameter(parameter || {}),
|
...cloneParameter(parameter || {}),
|
||||||
@@ -534,7 +545,7 @@ function normalizeSystemStartParameter(
|
|||||||
formDescription: asString(parameter?.formDescription),
|
formDescription: asString(parameter?.formDescription),
|
||||||
formPlaceholder: trimString(parameter?.formPlaceholder) || '请输入用户问题',
|
formPlaceholder: trimString(parameter?.formPlaceholder) || '请输入用户问题',
|
||||||
defaultValue: asString(parameter?.defaultValue),
|
defaultValue: asString(parameter?.defaultValue),
|
||||||
displayName: `流程开始 > ${formLabel}`,
|
displayName: `流程开始 > ${displayLabel}`,
|
||||||
nameDisabled: true,
|
nameDisabled: true,
|
||||||
dataTypeDisabled: true,
|
dataTypeDisabled: true,
|
||||||
deleteDisabled: true,
|
deleteDisabled: true,
|
||||||
@@ -637,11 +648,18 @@ function normalizeStartFormField(
|
|||||||
const options = isOptionFieldType(type)
|
const options = isOptionFieldType(type)
|
||||||
? ensureStringArray(field?.options || existingParameter?.enums)
|
? ensureStringArray(field?.options || existingParameter?.enums)
|
||||||
: [];
|
: [];
|
||||||
const requestedLabel =
|
const hasFieldLabel = Object.prototype.hasOwnProperty.call(
|
||||||
trimString(field?.label) ||
|
field || {},
|
||||||
trimString(existingParameter?.formLabel) ||
|
'label',
|
||||||
trimString(existingParameter?.displayName) ||
|
);
|
||||||
key;
|
const requestedLabel = isSystemField
|
||||||
|
? normalizeSystemStartFormLabel(
|
||||||
|
hasFieldLabel ? field?.label : existingParameter?.formLabel,
|
||||||
|
)
|
||||||
|
: trimString(field?.label) ||
|
||||||
|
trimString(existingParameter?.formLabel) ||
|
||||||
|
trimString(existingParameter?.displayName) ||
|
||||||
|
key;
|
||||||
const label =
|
const label =
|
||||||
!isSystemField &&
|
!isSystemField &&
|
||||||
isDefaultStartFormFieldLabel(requestedLabel) &&
|
isDefaultStartFormFieldLabel(requestedLabel) &&
|
||||||
@@ -796,8 +814,7 @@ export function normalizeStartFormSchema(
|
|||||||
{
|
{
|
||||||
id: trimString(systemParameter?.id),
|
id: trimString(systemParameter?.id),
|
||||||
key: SYSTEM_START_PARAM_NAME,
|
key: SYSTEM_START_PARAM_NAME,
|
||||||
label:
|
label: normalizeSystemStartFormLabel(systemParameter?.formLabel),
|
||||||
trimString(systemParameter?.formLabel) || SYSTEM_START_PARAM_LABEL,
|
|
||||||
type:
|
type:
|
||||||
normalizeSystemStartFormType(systemParameter?.formType) === 'input'
|
normalizeSystemStartFormType(systemParameter?.formType) === 'input'
|
||||||
? 'text'
|
? 'text'
|
||||||
@@ -933,6 +950,7 @@ export function updateStartFormField(
|
|||||||
!trimString(patch.label) &&
|
!trimString(patch.label) &&
|
||||||
patch.type != null &&
|
patch.type != null &&
|
||||||
isDefaultStartFormFieldLabel(field.label);
|
isDefaultStartFormFieldLabel(field.label);
|
||||||
|
const hasLabelPatch = Object.prototype.hasOwnProperty.call(patch, 'label');
|
||||||
return {
|
return {
|
||||||
...field,
|
...field,
|
||||||
...patch,
|
...patch,
|
||||||
@@ -947,10 +965,12 @@ export function updateStartFormField(
|
|||||||
)
|
)
|
||||||
: field.key),
|
: field.key),
|
||||||
label:
|
label:
|
||||||
trimString(patch.label) ||
|
field.systemReserved && hasLabelPatch
|
||||||
(shouldAutoRenameLabel
|
? normalizeSystemStartFormLabel(patch.label)
|
||||||
? getDefaultStartFormFieldLabel(nextType)
|
: trimString(patch.label) ||
|
||||||
: field.label),
|
(shouldAutoRenameLabel
|
||||||
|
? getDefaultStartFormFieldLabel(nextType)
|
||||||
|
: field.label),
|
||||||
type: nextType,
|
type: nextType,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -1450,28 +1470,115 @@ function replaceStartFieldReferenceValue(
|
|||||||
|
|
||||||
export function collectDownstreamNodeIds(rootNodeId: string, edges: Edge[]) {
|
export function collectDownstreamNodeIds(rootNodeId: string, edges: Edge[]) {
|
||||||
const nodeIds = new Set<string>();
|
const nodeIds = new Set<string>();
|
||||||
|
const adjacency = new Map<string, string[]>();
|
||||||
|
for (const edge of edges) {
|
||||||
|
if (!edge.source || !edge.target || edge.sourceHandle === 'loop_handle') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const targets = adjacency.get(edge.source);
|
||||||
|
if (targets) {
|
||||||
|
targets.push(edge.target);
|
||||||
|
} else {
|
||||||
|
adjacency.set(edge.source, [edge.target]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const visit = (nodeId: string) => {
|
const pendingNodeIds = [rootNodeId];
|
||||||
|
while (pendingNodeIds.length > 0) {
|
||||||
|
const nodeId = pendingNodeIds.pop();
|
||||||
if (!nodeId || nodeIds.has(nodeId)) {
|
if (!nodeId || nodeIds.has(nodeId)) {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
nodeIds.add(nodeId);
|
nodeIds.add(nodeId);
|
||||||
edges
|
const targets = adjacency.get(nodeId) || [];
|
||||||
.filter(
|
for (let index = targets.length - 1; index >= 0; index -= 1) {
|
||||||
(edge) => edge.source === nodeId && edge.sourceHandle !== 'loop_handle',
|
pendingNodeIds.push(targets[index]!);
|
||||||
)
|
}
|
||||||
.forEach((edge) => {
|
}
|
||||||
if (edge.target) {
|
|
||||||
visit(edge.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
visit(rootNodeId);
|
|
||||||
nodeIds.delete(rootNodeId);
|
nodeIds.delete(rootNodeId);
|
||||||
return Array.from(nodeIds);
|
return Array.from(nodeIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncReferenceParameterLabels(
|
||||||
|
parameters: Parameter[],
|
||||||
|
refPath: string,
|
||||||
|
displayName: string,
|
||||||
|
): Parameter[] {
|
||||||
|
let changed = false;
|
||||||
|
const nextParameters = parameters.map((parameter) => {
|
||||||
|
const currentChildren = Array.isArray(parameter.children)
|
||||||
|
? parameter.children
|
||||||
|
: [];
|
||||||
|
const nextChildren = currentChildren.length
|
||||||
|
? syncReferenceParameterLabels(currentChildren, refPath, displayName)
|
||||||
|
: currentChildren;
|
||||||
|
const matchesReference =
|
||||||
|
(parameter.autoManaged === true || parameter.refType === 'ref') &&
|
||||||
|
(trimString(parameter.name) === refPath ||
|
||||||
|
trimString(parameter.ref) === refPath);
|
||||||
|
const labelChanged =
|
||||||
|
matchesReference &&
|
||||||
|
(parameter.displayName !== displayName ||
|
||||||
|
parameter.formLabel !== displayName);
|
||||||
|
const childrenChanged = nextChildren !== currentChildren;
|
||||||
|
if (!labelChanged && !childrenChanged) {
|
||||||
|
return parameter;
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
return {
|
||||||
|
...parameter,
|
||||||
|
...(labelChanged ? { displayName, formLabel: displayName } : {}),
|
||||||
|
...(childrenChanged ? { children: nextChildren } : {}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return changed ? nextParameters : parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncStartFieldReferenceLabelsInNodes(
|
||||||
|
nodes: Node[],
|
||||||
|
startNodeId: string,
|
||||||
|
fieldKey: string,
|
||||||
|
) {
|
||||||
|
const startNode = nodes.find((node) => node.id === startNodeId);
|
||||||
|
const startParameters = Array.isArray(startNode?.data?.parameters)
|
||||||
|
? (startNode.data.parameters as Parameter[])
|
||||||
|
: [];
|
||||||
|
const sourceParameter = findParameterByName(startParameters, fieldKey);
|
||||||
|
if (!startNode || !sourceParameter) {
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refPath = `${startNodeId}.${fieldKey}`;
|
||||||
|
const displayName = `${getNodeTitle(startNode)} > ${getReferenceParameterLabel(sourceParameter)}`;
|
||||||
|
let changed = false;
|
||||||
|
const nextNodes = nodes.map((node) => {
|
||||||
|
const parameters = Array.isArray(node.data?.parameters)
|
||||||
|
? (node.data.parameters as Parameter[])
|
||||||
|
: [];
|
||||||
|
if (parameters.length === 0) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
const nextParameters = syncReferenceParameterLabels(
|
||||||
|
parameters,
|
||||||
|
refPath,
|
||||||
|
displayName,
|
||||||
|
);
|
||||||
|
if (nextParameters === parameters) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...((node.data || {}) as Record<string, any>),
|
||||||
|
parameters: nextParameters,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return changed ? nextNodes : nodes;
|
||||||
|
}
|
||||||
|
|
||||||
export function renameStartFieldReferencesInNodes(
|
export function renameStartFieldReferencesInNodes(
|
||||||
nodes: Node[],
|
nodes: Node[],
|
||||||
edges: Edge[],
|
edges: Edge[],
|
||||||
@@ -1482,35 +1589,41 @@ export function renameStartFieldReferencesInNodes(
|
|||||||
const normalizedStartNodeId = trimString(startNodeId);
|
const normalizedStartNodeId = trimString(startNodeId);
|
||||||
const normalizedCurrentKey = trimString(currentKey);
|
const normalizedCurrentKey = trimString(currentKey);
|
||||||
const normalizedNextKey = trimString(nextKey);
|
const normalizedNextKey = trimString(nextKey);
|
||||||
if (
|
if (!normalizedStartNodeId || !normalizedCurrentKey || !normalizedNextKey) {
|
||||||
!normalizedStartNodeId ||
|
|
||||||
!normalizedCurrentKey ||
|
|
||||||
!normalizedNextKey ||
|
|
||||||
normalizedCurrentKey === normalizedNextKey
|
|
||||||
) {
|
|
||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldRefPath = `${normalizedStartNodeId}.${normalizedCurrentKey}`;
|
const oldRefPath = `${normalizedStartNodeId}.${normalizedCurrentKey}`;
|
||||||
const newRefPath = `${normalizedStartNodeId}.${normalizedNextKey}`;
|
const newRefPath = `${normalizedStartNodeId}.${normalizedNextKey}`;
|
||||||
|
|
||||||
const nextNodes: Node[] = nodes.map((node) => {
|
const keyChanged = normalizedCurrentKey !== normalizedNextKey;
|
||||||
if (node.id === normalizedStartNodeId) {
|
const renamedNodes: Node[] = keyChanged
|
||||||
return node;
|
? nodes.map((node) => {
|
||||||
}
|
if (node.id === normalizedStartNodeId) {
|
||||||
const nextData = replaceStartFieldReferenceValue(
|
return node;
|
||||||
node.data,
|
}
|
||||||
oldRefPath,
|
const nextData = replaceStartFieldReferenceValue(
|
||||||
newRefPath,
|
node.data,
|
||||||
) as Record<string, any> | undefined;
|
oldRefPath,
|
||||||
if (nextData === node.data) {
|
newRefPath,
|
||||||
return node;
|
) as Record<string, any> | undefined;
|
||||||
}
|
if (nextData === node.data) {
|
||||||
return {
|
return node;
|
||||||
...node,
|
}
|
||||||
data: nextData ?? {},
|
return {
|
||||||
};
|
...node,
|
||||||
});
|
data: nextData ?? {},
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: nodes;
|
||||||
|
const nextNodes = syncStartFieldReferenceLabelsInNodes(
|
||||||
|
renamedNodes,
|
||||||
|
normalizedStartNodeId,
|
||||||
|
normalizedNextKey,
|
||||||
|
);
|
||||||
|
if (!keyChanged) {
|
||||||
|
return nextNodes;
|
||||||
|
}
|
||||||
|
|
||||||
const affectedNodeIds = collectDownstreamNodeIds(
|
const affectedNodeIds = collectDownstreamNodeIds(
|
||||||
normalizedStartNodeId,
|
normalizedStartNodeId,
|
||||||
|
|||||||
Reference in New Issue
Block a user