Commit c21783ed by siigure Committed by GitHub

chore(workflow): update workflowUtilsContext and add normalization fo… (#7324)

* chore(workflow): update workflowUtilsContext and add normalization for auto-fill references

* feat(workflow): enhance workflow data handling by filtering unselectable reference inputs
parent 210ab7ce
......@@ -108,7 +108,11 @@ export const useDebug = () => {
if (!hasError) {
onRemoveError();
const storeNodes = uiWorkflow2StoreWorkflow({ nodes, edges });
const storeNodes = uiWorkflow2StoreWorkflow({
nodes,
edges,
chatConfig: appDetail.chatConfig
});
return JSON.stringify(storeNodes);
}
......
// 工作流快照管理层
import React, { useCallback, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { createContext, useContextSelector } from 'use-context-selector';
import { useTranslation } from 'next-i18next';
import type { Node, Edge } from 'reactflow';
......@@ -61,7 +61,7 @@ type WorkflowSnapshotContextValue = {
};
export const WorkflowSnapshotContext = createContext<WorkflowSnapshotContextValue>({
past: [],
setPast: function (value: React.SetStateAction<WorkflowSnapshotsType[]>): void {
setPast: function (_value: React.SetStateAction<WorkflowSnapshotsType[]>): void {
throw new Error('Function not implemented.');
},
future: [],
......@@ -73,7 +73,7 @@ export const WorkflowSnapshotContext = createContext<WorkflowSnapshotContextValu
},
canUndo: false,
canRedo: false,
pushPastSnapshot: function (params: {
pushPastSnapshot: function (_params: {
pastNodes: Node[];
pastEdges: Edge[];
chatConfig: AppChatConfigType;
......@@ -82,10 +82,10 @@ export const WorkflowSnapshotContext = createContext<WorkflowSnapshotContextValu
}): boolean {
throw new Error('Function not implemented.');
},
onSwitchTmpVersion: function (data: WorkflowSnapshotsType, customTitle: string): boolean {
onSwitchTmpVersion: function (_data: WorkflowSnapshotsType, _customTitle: string): boolean {
throw new Error('Function not implemented.');
},
onSwitchCloudVersion: function (appVersion: AppVersionSchemaType): boolean {
onSwitchCloudVersion: function (_appVersion: AppVersionSchemaType): boolean {
throw new Error('Function not implemented.');
}
});
......@@ -98,10 +98,11 @@ export const WorkflowSnapshotProvider = ({ children }: { children: React.ReactNo
const { t } = useTranslation();
// 获取 WorkflowBufferDataContext 的数据
const { setEdges, setNodes, forbiddenSaveSnapshot } = useContextSelector(
WorkflowBufferDataContext,
(v) => v
);
const {
setEdges,
setNodes,
forbiddenSaveSnapshot: forbiddenSaveSnapshotRef
} = useContextSelector(WorkflowBufferDataContext, (v) => v);
// 获取 AppContext 的 setAppDetail
const setAppDetail = useContextSelector(AppContext, (v) => v.setAppDetail);
......@@ -109,6 +110,10 @@ export const WorkflowSnapshotProvider = ({ children }: { children: React.ReactNo
const [past, setPast] = useState<WorkflowSnapshotsType[]>([]);
const [future, setFuture] = useState<WorkflowSnapshotsType[]>([]);
const pushPastSnapshotRef = useRef<WorkflowSnapshotContextValue['pushPastSnapshot'] | undefined>(
undefined
);
// 待保存快照队列机制 - 解决竞态条件,确保数据不丢失
const pendingSnapshotRef = useRef<{
data: {
......@@ -154,8 +159,8 @@ export const WorkflowSnapshotProvider = ({ children }: { children: React.ReactNo
}
// 3. 处理被阻塞的快照
if (forbiddenSaveSnapshot.current) {
forbiddenSaveSnapshot.current = false;
if (forbiddenSaveSnapshotRef.current) {
forbiddenSaveSnapshotRef.current = false;
console.warn('[Snapshot] Snapshot creation blocked, adding to pending queue');
// 将快照加入待处理队列
......@@ -169,7 +174,7 @@ export const WorkflowSnapshotProvider = ({ children }: { children: React.ReactNo
pendingSnapshotRef.current.timeoutId = setTimeout(() => {
if (pendingSnapshotRef.current?.data) {
console.log('[Snapshot] Processing pending snapshot from queue');
pushPastSnapshot(pendingSnapshotRef.current.data);
pushPastSnapshotRef.current?.(pendingSnapshotRef.current.data);
pendingSnapshotRef.current = { data: null };
} else {
console.log('[Snapshot] No pending snapshot to process');
......@@ -236,12 +241,16 @@ export const WorkflowSnapshotProvider = ({ children }: { children: React.ReactNo
return false;
}
},
[past, forbiddenSaveSnapshot]
[past, forbiddenSaveSnapshotRef]
);
useEffect(() => {
pushPastSnapshotRef.current = pushPastSnapshot;
}, [pushPastSnapshot]);
const undo = useCallback(() => {
if (past.length > 1) {
forbiddenSaveSnapshot.current = true;
forbiddenSaveSnapshotRef.current = true;
// Current version is the first one, so we need to reset the second one
const firstPast = past[1];
resetSnapshot(firstPast);
......@@ -249,7 +258,7 @@ export const WorkflowSnapshotProvider = ({ children }: { children: React.ReactNo
setFuture((future) => [past[0], ...future]);
setPast((past) => past.slice(1));
}
}, [past, resetSnapshot, forbiddenSaveSnapshot]);
}, [past, resetSnapshot, forbiddenSaveSnapshotRef]);
const redo = useCallback(() => {
if (!future[0]) return;
......@@ -257,13 +266,13 @@ export const WorkflowSnapshotProvider = ({ children }: { children: React.ReactNo
const futureState = future[0];
if (futureState) {
forbiddenSaveSnapshot.current = true;
forbiddenSaveSnapshotRef.current = true;
setPast((past) => [futureState, ...past]);
setFuture((future) => future.slice(1));
resetSnapshot(futureState);
}
}, [future, resetSnapshot, forbiddenSaveSnapshot]);
}, [future, resetSnapshot, forbiddenSaveSnapshotRef]);
const onSwitchTmpVersion = useCallback(
(params: WorkflowSnapshotsType, customTitle: string) => {
......@@ -286,8 +295,8 @@ export const WorkflowSnapshotProvider = ({ children }: { children: React.ReactNo
const onSwitchCloudVersion = useCallback(
(appVersion: AppVersionSchemaType) => {
const nodes = appVersion.nodes.map((item) => storeNode2FlowNode({ item, t }));
const edges = appVersion.edges.map((item) => storeEdge2RenderEdge({ edge: item }));
const nodes = appVersion.nodes.map((item) => storeNode2FlowNode({ item, t }));
const chatConfig = appVersion.chatConfig;
resetSnapshot({
......
......@@ -167,8 +167,8 @@ export const WorkflowUtilsProvider = ({ children }: { children: ReactNode }) =>
// 将 UI 流程数据转换为存储格式
const flowData2StoreData = useCallback(() => {
const nodes = getNodes();
return uiWorkflow2StoreWorkflow({ nodes, edges });
}, [getNodes, edges]);
return uiWorkflow2StoreWorkflow({ nodes, edges, chatConfig: appDetail.chatConfig });
}, [getNodes, edges, appDetail.chatConfig]);
// 转换并验证工作流数据
const flowData2StoreDataAndCheck = useCallback(
......@@ -214,7 +214,11 @@ export const WorkflowUtilsProvider = ({ children }: { children: ReactNode }) =>
if (!hasError) {
onRemoveError();
const storeWorkflow = uiWorkflow2StoreWorkflow({ nodes, edges });
const storeWorkflow = uiWorkflow2StoreWorkflow({
nodes,
edges,
chatConfig: appDetail.chatConfig
});
return storeWorkflow;
}
......@@ -249,6 +253,7 @@ export const WorkflowUtilsProvider = ({ children }: { children: ReactNode }) =>
onUpdateNodeError,
showSandbox,
enableSandbox,
appDetail.chatConfig,
toast
]
);
......
import { getNodeAllSource } from '@/web/core/workflow/utils';
import { type AppDetailType } from '@fastgpt/global/core/app/type';
import { getNodeAllSource, workflowReferenceValueIsSelectable } from '@/web/core/workflow/utils';
import { type AppChatConfigType, type AppDetailType } from '@fastgpt/global/core/app/type';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import {
FlowNodeOutputTypeEnum,
......@@ -10,16 +10,30 @@ import {
type FlowNodeItemType,
type StoreNodeItemType
} from '@fastgpt/global/core/workflow/type/node';
import type {
FlowNodeInputItemType,
ReferenceItemValueType,
ReferenceValueType
} from '@fastgpt/global/core/workflow/type/io';
import { nodeInputIsReference } from '@fastgpt/global/core/workflow/utils';
import { type TFunction } from 'i18next';
import { type Edge, type Node } from 'reactflow';
export const uiWorkflow2StoreWorkflow = ({
nodes,
edges
edges,
chatConfig
}: {
nodes: Node<FlowNodeItemType, string | undefined>[];
edges: Edge<any>[];
chatConfig?: AppChatConfigType;
}) => {
const getNodeById = (nodeId: string | null | undefined) =>
nodes.find((node) => node.data.nodeId === nodeId)?.data;
const systemConfigNode = nodes.find(
(node) => node.data.flowNodeType === FlowNodeTypeEnum.systemConfig
)?.data;
const formatNodes: StoreNodeItemType[] = nodes.map((item) => ({
nodeId: item.data.nodeId,
parentNodeId: item.data.parentNodeId,
......@@ -31,7 +45,14 @@ export const uiWorkflow2StoreWorkflow = ({
showStatus: item.data.showStatus,
position: item.position,
version: item.data.version,
inputs: item.data.inputs,
inputs: filterUnselectableReferenceInputs({
node: item.data,
inputs: item.data.inputs,
edges,
chatConfig,
systemConfigNode,
getNodeById
}),
outputs: item.data.outputs,
isFolded: item.data.isFolded,
pluginId: item.data.pluginId,
......@@ -62,6 +83,73 @@ export const uiWorkflow2StoreWorkflow = ({
};
};
const emptyT = ((key: string) => key) as TFunction;
/**
* 保存时仅持久化当前引用选择器仍能选中的引用项。
* 已删除来源、已删除输出、类型不再匹配的引用在 UI 上不会展示标签,也不应继续写入 JSON。
*/
const filterUnselectableReferenceInputs = ({
node,
inputs,
edges,
chatConfig,
systemConfigNode,
getNodeById
}: {
node: FlowNodeItemType;
inputs: FlowNodeInputItemType[];
edges: Edge<any>[];
chatConfig?: AppChatConfigType;
systemConfigNode?: FlowNodeItemType;
getNodeById: (nodeId: string | null | undefined) => FlowNodeItemType | undefined;
}) => {
return inputs.map((input) => {
if (!nodeInputIsReference(input)) return input;
const sourceNodes = getNodeAllSource({
nodeId: node.nodeId,
systemConfigNode,
getNodeById,
edges,
chatConfig: chatConfig ?? ({} as AppChatConfigType),
t: emptyT
});
const value = input.value as ReferenceValueType | undefined;
if (!Array.isArray(value)) return input;
if (typeof value[0] === 'string') {
const keepValue = workflowReferenceValueIsSelectable({
value,
sourceNodes,
valueType: input.valueType
});
return keepValue
? input
: {
...input,
value: undefined
};
}
const filteredValue = (value as ReferenceItemValueType[]).filter((item) =>
workflowReferenceValueIsSelectable({
value: item,
sourceNodes,
valueType: input.valueType
})
);
if (filteredValue.length === value.length) return input;
return {
...input,
value: filteredValue
};
});
};
export const filterExportModules = (modules: StoreNodeItemType[]) => {
modules.forEach((module) => {
// dataset - remove select dataset value
......
......@@ -409,14 +409,6 @@ const isUnsetReferenceValue = (value: unknown) => {
return false;
};
/** 引用曾经有效配置过,但目标节点或输出已不存在(如上游节点被删除)。 */
const isStaleReferenceValue = (value: unknown, context: WorkflowCheckContext) => {
if (!isValidReferenceValueFormat(value)) return false;
const [refNodeId, refOutputId] = value;
if (!refNodeId || !refOutputId) return false;
return !referenceValueIsLive(value as ReferenceItemValueType, context);
};
const isEmptyReferenceInputValue = (value: unknown, isArrayType: boolean) => {
if (isArrayType) {
return !Array.isArray(value) || value.length === 0;
......@@ -794,33 +786,6 @@ export const checkWorkflowNodeIssues = ({
inputKey: input.key
});
}
if (isReferenceInput) {
if (isArrayReference) {
const value = Array.isArray(input.value) ? input.value : [];
const hasStaleReference = value.some((item) => isStaleReferenceValue(item, context));
if (hasStaleReference) {
addIssue({
node,
code: 'invalid_reference',
message: getWorkflowCheckIssueMessage('invalid_reference', t, {
inputName: getInputLabel(input, t)
}),
inputKey: input.key
});
}
} else if (isStaleReferenceValue(input.value, context)) {
addIssue({
node,
code: 'invalid_reference',
message: getWorkflowCheckIssueMessage('invalid_reference', t, {
inputName: getInputLabel(input, t)
}),
inputKey: input.key
});
}
}
});
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment