mirror of
https://github.com/labring/FastGPT.git
synced 2025-08-01 03:48:24 +00:00
4.8 preview (#1288)
* Revert "lafAccount add pat & re request when token invalid (#76)" (#77) This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be. * perf: workflow ux * system config * Newflow (#89) * docs: Add doc for Xinference (#1266) Signed-off-by: Carson Yang <yangchuansheng33@gmail.com> * Revert "lafAccount add pat & re request when token invalid (#76)" (#77) This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be. * perf: workflow ux * system config * Revert "lafAccount add pat & re request when token invalid (#76)" (#77) This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be. * Revert "lafAccount add pat & re request when token invalid (#76)" (#77) This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be. * Revert "lafAccount add pat & re request when token invalid (#76)" (#77) This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be. * rename code * move code * update flow * input type selector * perf: workflow runtime * feat: node adapt newflow * feat: adapt plugin * feat: 360 connection * check workflow * perf: flow 性能 * change plugin input type (#81) * change plugin input type * plugin label mode * perf: nodecard * debug * perf: debug ui * connection ui * change workflow ui (#82) * feat: workflow debug * adapt openAPI for new workflow (#83) * adapt openAPI for new workflow * i18n * perf: plugin debug * plugin input ui * delete * perf: global variable select * fix rebase * perf: workflow performance * feat: input render type icon * input icon * adapt flow (#84) * adapt newflow * temp * temp * fix * feat: app schedule trigger * feat: app schedule trigger * perf: schedule ui * feat: ioslatevm run js code * perf: workflow varialbe table ui * feat: adapt simple mode * feat: adapt input params * output * feat: adapt tamplate * fix: ts * add if-else module (#86) * perf: worker * if else node * perf: tiktoken worker * fix: ts * perf: tiktoken * fix if-else node (#87) * fix if-else node * type * fix * perf: audio render * perf: Parallel worker * log * perf: if else node * adapt plugin * prompt * perf: reference ui * reference ui * handle ux * template ui and plugin tool * adapt v1 workflow * adapt v1 workflow completions * perf: time variables * feat: workflow keyboard shortcuts * adapt v1 workflow * update workflow example doc (#88) * fix: simple mode select tool --------- Signed-off-by: Carson Yang <yangchuansheng33@gmail.com> Co-authored-by: Carson Yang <yangchuansheng33@gmail.com> Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com> * doc * perf: extract node * extra node field * update plugin version * doc * variable * change doc & fix prompt editor (#90) * fold workflow code * value type label --------- Signed-off-by: Carson Yang <yangchuansheng33@gmail.com> Co-authored-by: Carson Yang <yangchuansheng33@gmail.com> Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { SseResponseEventEnum } from '@fastgpt/global/core/module/runtime/constants';
|
||||
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
|
||||
import type { StartChatFnProps } from '@/components/ChatBox/type.d';
|
||||
import { getToken } from '@/web/support/user/auth';
|
||||
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/module/runtime/constants';
|
||||
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
// refer to https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web
|
||||
@@ -109,7 +109,7 @@ export const streamFetch = ({
|
||||
try {
|
||||
// auto complete variables
|
||||
const variables = data?.variables || {};
|
||||
variables.cTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
variables.cTime = dayjs().format('YYYY-MM-DD HH:mm:ss dddd');
|
||||
|
||||
const requestData = {
|
||||
method: 'POST',
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* copy text data
|
||||
@@ -8,12 +9,8 @@ export const useCopyData = () => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
|
||||
return {
|
||||
copyData: async (
|
||||
data: string,
|
||||
title: string | null = t('common.Copy Successful'),
|
||||
duration = 1000
|
||||
) => {
|
||||
const copyData = useCallback(
|
||||
async (data: string, title: string | null = t('common.Copy Successful'), duration = 1000) => {
|
||||
try {
|
||||
if (navigator.clipboard) {
|
||||
await navigator.clipboard.writeText(data);
|
||||
@@ -31,11 +28,18 @@ export const useCopyData = () => {
|
||||
document.body?.removeChild(textarea);
|
||||
}
|
||||
|
||||
toast({
|
||||
title,
|
||||
status: 'success',
|
||||
duration
|
||||
});
|
||||
}
|
||||
if (title) {
|
||||
toast({
|
||||
title,
|
||||
status: 'success',
|
||||
duration
|
||||
});
|
||||
}
|
||||
},
|
||||
[t, toast]
|
||||
);
|
||||
|
||||
return {
|
||||
copyData
|
||||
};
|
||||
};
|
||||
|
@@ -51,7 +51,7 @@ export const useSpeech = (props?: OutLinkChatAuthProps & { appId?: string }) =>
|
||||
}, []);
|
||||
|
||||
const startSpeak = async (onFinish: (text: string) => void) => {
|
||||
if (!navigator.mediaDevices.getUserMedia) {
|
||||
if (!navigator?.mediaDevices?.getUserMedia) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('common.speech.not support')
|
||||
|
@@ -2,23 +2,20 @@ export enum EventNameEnum {
|
||||
sendQuestion = 'sendQuestion',
|
||||
editQuestion = 'editQuestion',
|
||||
|
||||
// flow
|
||||
requestFlowEvent = 'requestFlowEvent',
|
||||
requestFlowStore = 'requestFlowStore',
|
||||
receiveFlowStore = 'receiveFlowStore'
|
||||
requestWorkflowStore = 'requestWorkflowStore',
|
||||
receiveWorkflowStore = 'receiveWorkflowStore'
|
||||
}
|
||||
type EventNameType = `${EventNameEnum}`;
|
||||
|
||||
export const eventBus = {
|
||||
list: new Map<EventNameType, Function>(),
|
||||
on: function (name: EventNameType, fn: Function) {
|
||||
list: new Map<EventNameEnum, Function>(),
|
||||
on: function (name: EventNameEnum, fn: Function) {
|
||||
this.list.set(name, fn);
|
||||
},
|
||||
emit: function (name: EventNameType, data: Record<string, any> = {}) {
|
||||
emit: function (name: EventNameEnum, data: Record<string, any> = {}) {
|
||||
const fn = this.list.get(name);
|
||||
fn && fn(data);
|
||||
},
|
||||
off: function (name: EventNameType) {
|
||||
off: function (name: EventNameEnum) {
|
||||
this.list.delete(name);
|
||||
}
|
||||
};
|
||||
|
@@ -5,6 +5,7 @@ import type { AppTTSConfigType } from '@fastgpt/global/core/app/type.d';
|
||||
import { TTSTypeEnum } from '@/constants/app';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat.d';
|
||||
import { getToken } from '@/web/support/user/auth';
|
||||
|
||||
const contentType = 'audio/mpeg';
|
||||
const splitMarker = 'SPLIT_MARKER';
|
||||
@@ -13,8 +14,7 @@ export const useAudioPlay = (props?: OutLinkChatAuthProps & { ttsConfig?: AppTTS
|
||||
const { t } = useTranslation();
|
||||
const { ttsConfig, shareId, outLinkUid, teamId, teamToken } = props || {};
|
||||
const { toast } = useToast();
|
||||
const audioRef = useRef<HTMLAudioElement>(new Audio());
|
||||
const audio = audioRef.current;
|
||||
const audioRef = useRef<HTMLAudioElement>();
|
||||
const [audioLoading, setAudioLoading] = useState(false);
|
||||
const [audioPlaying, setAudioPlaying] = useState(false);
|
||||
const audioController = useRef(new AbortController());
|
||||
@@ -40,7 +40,8 @@ export const useAudioPlay = (props?: OutLinkChatAuthProps & { ttsConfig?: AppTTS
|
||||
const response = await fetch('/api/core/chat/item/getSpeech', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
token: getToken()
|
||||
},
|
||||
signal: audioController.current.signal,
|
||||
body: JSON.stringify({
|
||||
@@ -93,30 +94,39 @@ export const useAudioPlay = (props?: OutLinkChatAuthProps & { ttsConfig?: AppTTS
|
||||
window.speechSynthesis?.cancel();
|
||||
audioController.current.abort('');
|
||||
} catch (error) {}
|
||||
if (audio) {
|
||||
audio.pause();
|
||||
audio.src = '';
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
audioRef.current.src = '';
|
||||
}
|
||||
setAudioPlaying(false);
|
||||
}, [audio]);
|
||||
}, []);
|
||||
|
||||
/* Perform a voice playback */
|
||||
const playAudioByText = useCallback(
|
||||
async ({ text, buffer }: { text: string; buffer?: Uint8Array }) => {
|
||||
const playAudioBuffer = (buffer: Uint8Array) => {
|
||||
if (!audioRef.current) return;
|
||||
const audioUrl = URL.createObjectURL(new Blob([buffer], { type: 'audio/mpeg' }));
|
||||
|
||||
audio.src = audioUrl;
|
||||
audio.play();
|
||||
audioRef.current.src = audioUrl;
|
||||
audioRef.current.play();
|
||||
};
|
||||
const readAudioStream = (stream: ReadableStream<Uint8Array>) => {
|
||||
if (!audio) return;
|
||||
if (!audioRef.current) return;
|
||||
|
||||
if (!MediaSource) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: t('core.chat.Audio Not Support')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Create media source and play audio
|
||||
const ms = new MediaSource();
|
||||
const url = URL.createObjectURL(ms);
|
||||
audio.src = url;
|
||||
audio.play();
|
||||
audioRef.current.src = url;
|
||||
audioRef.current.play();
|
||||
|
||||
let u8Arr: Uint8Array = new Uint8Array();
|
||||
return new Promise<Uint8Array>(async (resolve, reject) => {
|
||||
@@ -132,7 +142,7 @@ export const useAudioPlay = (props?: OutLinkChatAuthProps & { ttsConfig?: AppTTS
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done || audio.paused) {
|
||||
if (done || audioRef.current?.paused) {
|
||||
resolve(u8Arr);
|
||||
if (sourceBuffer.updating) {
|
||||
await new Promise((resolve) => (sourceBuffer.onupdateend = resolve));
|
||||
@@ -161,7 +171,7 @@ export const useAudioPlay = (props?: OutLinkChatAuthProps & { ttsConfig?: AppTTS
|
||||
cancelAudio();
|
||||
|
||||
// tts play
|
||||
if (audio && ttsConfig?.type === TTSTypeEnum.model) {
|
||||
if (audioRef.current && ttsConfig?.type === TTSTypeEnum.model) {
|
||||
/* buffer tts */
|
||||
if (buffer) {
|
||||
playAudioBuffer(buffer);
|
||||
@@ -188,7 +198,7 @@ export const useAudioPlay = (props?: OutLinkChatAuthProps & { ttsConfig?: AppTTS
|
||||
}
|
||||
});
|
||||
},
|
||||
[audio, cancelAudio, getAudioStream, playWebAudio, t, toast, ttsConfig?.type]
|
||||
[cancelAudio, getAudioStream, playWebAudio, t, toast, ttsConfig?.type]
|
||||
);
|
||||
|
||||
// segmented params
|
||||
@@ -199,7 +209,13 @@ export const useAudioPlay = (props?: OutLinkChatAuthProps & { ttsConfig?: AppTTS
|
||||
|
||||
/* Segmented voice playback */
|
||||
const startSegmentedAudio = useCallback(async () => {
|
||||
if (!audio) return;
|
||||
if (!audioRef.current) return;
|
||||
if (!MediaSource) {
|
||||
return toast({
|
||||
status: 'error',
|
||||
title: t('core.chat.Audio Not Support')
|
||||
});
|
||||
}
|
||||
cancelAudio();
|
||||
|
||||
/* reset all source */
|
||||
@@ -223,15 +239,15 @@ export const useAudioPlay = (props?: OutLinkChatAuthProps & { ttsConfig?: AppTTS
|
||||
const ms = new MediaSource();
|
||||
segmentedMediaSource.current = ms;
|
||||
const url = URL.createObjectURL(ms);
|
||||
audio.src = url;
|
||||
audio.play();
|
||||
audioRef.current.src = url;
|
||||
audioRef.current.play();
|
||||
|
||||
await new Promise((resolve) => {
|
||||
ms.onsourceopen = resolve;
|
||||
});
|
||||
const sourceBuffer = ms.addSourceBuffer(contentType);
|
||||
segmentedSourceBuffer.current = sourceBuffer;
|
||||
}, [audio, cancelAudio]);
|
||||
}, [cancelAudio, t, toast]);
|
||||
const finishSegmentedAudio = useCallback(() => {
|
||||
appendAudioPromise.current = appendAudioPromise.current.finally(() => {
|
||||
if (segmentedMediaSource.current?.readyState === 'open') {
|
||||
@@ -256,7 +272,7 @@ export const useAudioPlay = (props?: OutLinkChatAuthProps & { ttsConfig?: AppTTS
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done || !audio?.played) {
|
||||
if (done || !audioRef.current?.played) {
|
||||
buffer.updating && (await new Promise((resolve) => (buffer.onupdateend = resolve)));
|
||||
return resolve(u8Arr);
|
||||
}
|
||||
@@ -273,7 +289,7 @@ export const useAudioPlay = (props?: OutLinkChatAuthProps & { ttsConfig?: AppTTS
|
||||
}
|
||||
});
|
||||
},
|
||||
[audio?.played, getAudioStream, segmentedSourceBuffer]
|
||||
[getAudioStream, segmentedSourceBuffer]
|
||||
);
|
||||
/* split audio text and fetch tts */
|
||||
const splitText2Audio = useCallback(
|
||||
@@ -314,6 +330,9 @@ export const useAudioPlay = (props?: OutLinkChatAuthProps & { ttsConfig?: AppTTS
|
||||
|
||||
// listen audio status
|
||||
useEffect(() => {
|
||||
const audio = new Audio();
|
||||
audioRef.current = audio;
|
||||
|
||||
audio.onplay = () => {
|
||||
setAudioPlaying(true);
|
||||
};
|
||||
@@ -341,7 +360,7 @@ export const useAudioPlay = (props?: OutLinkChatAuthProps & { ttsConfig?: AppTTS
|
||||
}, []);
|
||||
|
||||
return {
|
||||
audio,
|
||||
audio: audioRef.current,
|
||||
audioLoading,
|
||||
audioPlaying,
|
||||
setAudioPlaying,
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
|
||||
import { ModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
|
||||
export function checkChatSupportSelectFileByChatModels(models: string[] = []) {
|
||||
@@ -14,10 +14,11 @@ export function checkChatSupportSelectFileByChatModels(models: string[] = []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function checkChatSupportSelectFileByModules(modules: ModuleItemType[] = []) {
|
||||
export function checkChatSupportSelectFileByModules(modules: StoreNodeItemType[] = []) {
|
||||
const chatModules = modules.filter(
|
||||
(item) =>
|
||||
item.flowType === FlowNodeTypeEnum.chatNode || item.flowType === FlowNodeTypeEnum.tools
|
||||
item.flowNodeType === FlowNodeTypeEnum.chatNode ||
|
||||
item.flowNodeType === FlowNodeTypeEnum.tools
|
||||
);
|
||||
const models: string[] = chatModules.map(
|
||||
(item) => item.inputs.find((item) => item.key === 'model')?.value || ''
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { GET, POST, DELETE, PUT } from '@/web/common/api/request';
|
||||
import { FlowNodeTemplateType } from '@fastgpt/global/core/module/type';
|
||||
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { PluginTypeEnum } from '@fastgpt/global/core/plugin/constants';
|
||||
import {
|
||||
@@ -36,6 +36,6 @@ export const getSystemPlugTemplates = () =>
|
||||
GET<FlowNodeTemplateType[]>('/core/plugin/pluginTemplate/getSystemPluginTemplates');
|
||||
|
||||
export const getPreviewPluginModule = (id: string) =>
|
||||
GET<FlowNodeTemplateType>('/core/plugin/getPreviewModule', { id });
|
||||
GET<FlowNodeTemplateType>('/core/plugin/getPreviewNode', { id });
|
||||
export const getOnePlugin = (id: string) => GET<PluginItemSchema>('/core/plugin/detail', { id });
|
||||
export const delOnePlugin = (pluginId: string) => DELETE('/core/plugin/delete', { pluginId });
|
||||
|
501
projects/app/src/web/core/workflow/adapt.ts
Normal file
501
projects/app/src/web/core/workflow/adapt.ts
Normal file
@@ -0,0 +1,501 @@
|
||||
import {
|
||||
FlowNodeTemplateTypeEnum,
|
||||
NodeInputKeyEnum,
|
||||
NodeOutputKeyEnum,
|
||||
WorkflowIOValueTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/constants';
|
||||
import {
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowNodeOutputTypeEnum,
|
||||
FlowNodeTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { getHandleConfig } from '@fastgpt/global/core/workflow/template/utils';
|
||||
import {
|
||||
FlowNodeItemType,
|
||||
FlowNodeTemplateType,
|
||||
StoreNodeItemType
|
||||
} from '@fastgpt/global/core/workflow/type';
|
||||
import { VARIABLE_NODE_ID } from './constants';
|
||||
import { getHandleId, splitGuideModule } from '@fastgpt/global/core/workflow/utils';
|
||||
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
import { LLMModelTypeEnum } from '@fastgpt/global/core/ai/constants';
|
||||
import {
|
||||
FlowNodeInputItemType,
|
||||
FlowNodeOutputItemType
|
||||
} from '@fastgpt/global/core/workflow/type/io';
|
||||
import { PluginTypeEnum } from '@fastgpt/global/core/plugin/constants';
|
||||
|
||||
export const systemConfigNode2VariableNode = (node: FlowNodeItemType) => {
|
||||
const template: FlowNodeTemplateType = {
|
||||
id: FlowNodeTypeEnum.globalVariable,
|
||||
templateType: FlowNodeTemplateTypeEnum.other,
|
||||
flowNodeType: FlowNodeTypeEnum.emptyNode,
|
||||
sourceHandle: getHandleConfig(false, false, false, false),
|
||||
targetHandle: getHandleConfig(false, false, false, false),
|
||||
avatar: '/imgs/workflow/variable.png',
|
||||
name: '全局变量',
|
||||
intro: '',
|
||||
unique: true,
|
||||
forbidDelete: true,
|
||||
inputs: [],
|
||||
outputs: []
|
||||
};
|
||||
|
||||
const { variableModules } = splitGuideModule(node);
|
||||
|
||||
const variableNode: FlowNodeItemType = {
|
||||
nodeId: VARIABLE_NODE_ID,
|
||||
...template,
|
||||
outputs: variableModules.map((item) => ({
|
||||
id: item.key,
|
||||
type: FlowNodeOutputTypeEnum.dynamic,
|
||||
label: item.label,
|
||||
key: item.key,
|
||||
valueType: WorkflowIOValueTypeEnum.any
|
||||
}))
|
||||
};
|
||||
|
||||
return variableNode;
|
||||
};
|
||||
|
||||
/* adapt v1 workfwlo */
|
||||
enum InputTypeEnum {
|
||||
triggerAndFinish = 'triggerAndFinish',
|
||||
systemInput = 'systemInput', // history, userChatInput, variableInput
|
||||
|
||||
input = 'input', // one line input
|
||||
numberInput = 'numberInput',
|
||||
select = 'select',
|
||||
slider = 'slider',
|
||||
target = 'target', // data input
|
||||
switch = 'switch',
|
||||
|
||||
// editor
|
||||
textarea = 'textarea',
|
||||
JSONEditor = 'JSONEditor',
|
||||
|
||||
addInputParam = 'addInputParam', // params input
|
||||
|
||||
selectApp = 'selectApp',
|
||||
|
||||
// chat special input
|
||||
aiSettings = 'aiSettings',
|
||||
|
||||
// ai model select
|
||||
selectLLMModel = 'selectLLMModel',
|
||||
settingLLMModel = 'settingLLMModel',
|
||||
|
||||
// dataset special input
|
||||
selectDataset = 'selectDataset',
|
||||
selectDatasetParamsModal = 'selectDatasetParamsModal',
|
||||
settingDatasetQuotePrompt = 'settingDatasetQuotePrompt',
|
||||
|
||||
hidden = 'hidden',
|
||||
custom = 'custom'
|
||||
}
|
||||
enum FlowTypeEnum {
|
||||
userGuide = 'userGuide',
|
||||
questionInput = 'questionInput',
|
||||
chatNode = 'chatNode',
|
||||
|
||||
datasetSearchNode = 'datasetSearchNode',
|
||||
datasetConcatNode = 'datasetConcatNode',
|
||||
|
||||
answerNode = 'answerNode',
|
||||
classifyQuestion = 'classifyQuestion',
|
||||
contentExtract = 'contentExtract',
|
||||
httpRequest468 = 'httpRequest468',
|
||||
runApp = 'app',
|
||||
pluginModule = 'pluginModule',
|
||||
pluginInput = 'pluginInput',
|
||||
pluginOutput = 'pluginOutput',
|
||||
queryExtension = 'cfr',
|
||||
tools = 'tools',
|
||||
stopTool = 'stopTool',
|
||||
lafModule = 'lafModule'
|
||||
}
|
||||
enum OutputTypeEnum {
|
||||
answer = 'answer',
|
||||
source = 'source',
|
||||
hidden = 'hidden',
|
||||
|
||||
addOutputParam = 'addOutputParam'
|
||||
}
|
||||
type V1WorkflowType = {
|
||||
name: string;
|
||||
avatar?: string;
|
||||
intro?: string;
|
||||
moduleId: string;
|
||||
position?: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
flowType: FlowTypeEnum;
|
||||
showStatus?: boolean;
|
||||
inputs: {
|
||||
valueType?: WorkflowIOValueTypeEnum; // data type
|
||||
type: InputTypeEnum; // Node Type. Decide on a render style
|
||||
key: `${NodeInputKeyEnum}` | string;
|
||||
value?: any;
|
||||
label: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
toolDescription?: string; // If this field is not empty, it is entered as a tool
|
||||
|
||||
edit?: boolean; // Whether to allow editing
|
||||
editField?: {
|
||||
inputType?: boolean;
|
||||
required?: boolean;
|
||||
isToolInput?: boolean;
|
||||
name?: boolean;
|
||||
key?: boolean;
|
||||
description?: boolean;
|
||||
dataType?: boolean;
|
||||
defaultValue?: boolean;
|
||||
};
|
||||
defaultEditField?: {
|
||||
inputType?: InputTypeEnum; // input type
|
||||
outputType?: `${FlowNodeOutputTypeEnum}`;
|
||||
required?: boolean;
|
||||
key?: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
valueType?: WorkflowIOValueTypeEnum;
|
||||
isToolInput?: boolean;
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
connected?: boolean; // There are incoming data
|
||||
|
||||
showTargetInApp?: boolean;
|
||||
showTargetInPlugin?: boolean;
|
||||
|
||||
hideInApp?: boolean;
|
||||
hideInPlugin?: boolean;
|
||||
|
||||
placeholder?: string; // input,textarea
|
||||
|
||||
list?: { label: string; value: any }[]; // select
|
||||
|
||||
markList?: { label: string; value: any }[]; // slider
|
||||
step?: number; // slider
|
||||
max?: number; // slider, number input
|
||||
min?: number; // slider, number input
|
||||
|
||||
llmModelType?: `${LLMModelTypeEnum}`;
|
||||
}[];
|
||||
outputs: {
|
||||
type?: OutputTypeEnum;
|
||||
key: `${NodeOutputKeyEnum}` | string;
|
||||
valueType?: WorkflowIOValueTypeEnum;
|
||||
|
||||
label?: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
defaultValue?: any;
|
||||
|
||||
edit?: boolean;
|
||||
editField?: {
|
||||
inputType?: boolean;
|
||||
required?: boolean;
|
||||
isToolInput?: boolean;
|
||||
name?: boolean;
|
||||
key?: boolean;
|
||||
description?: boolean;
|
||||
dataType?: boolean;
|
||||
defaultValue?: boolean;
|
||||
};
|
||||
defaultEditField?: {
|
||||
inputType?: `${FlowNodeInputTypeEnum}`; // input type
|
||||
outputType?: `${FlowNodeOutputTypeEnum}`;
|
||||
required?: boolean;
|
||||
key?: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
valueType?: `${WorkflowIOValueTypeEnum}`;
|
||||
isToolInput?: boolean;
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
targets: { moduleId: string; key: string }[];
|
||||
}[];
|
||||
|
||||
// runTime field
|
||||
isEntry?: boolean;
|
||||
pluginType?: `${PluginTypeEnum}`;
|
||||
parentId?: string;
|
||||
};
|
||||
export const v1Workflow2V2 = (
|
||||
nodes: V1WorkflowType[]
|
||||
): {
|
||||
nodes: StoreNodeItemType[];
|
||||
edges: StoreEdgeItemType[];
|
||||
} => {
|
||||
let copyNodes = JSON.parse(JSON.stringify(nodes)) as V1WorkflowType[];
|
||||
|
||||
// 只保留1个开始节点
|
||||
copyNodes = copyNodes.filter((node, index, self) => {
|
||||
if (node.flowType === FlowTypeEnum.questionInput) {
|
||||
return index === self.findIndex((item) => item.flowType === FlowTypeEnum.questionInput);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const newNodes: StoreNodeItemType[] = copyNodes.map((node) => {
|
||||
// flowNodeType adapt
|
||||
const nodeTypeMap = {
|
||||
[FlowTypeEnum.userGuide]: FlowNodeTypeEnum.systemConfig,
|
||||
[FlowTypeEnum.questionInput]: FlowNodeTypeEnum.workflowStart,
|
||||
[FlowTypeEnum.chatNode]: FlowNodeTypeEnum.chatNode,
|
||||
[FlowTypeEnum.datasetSearchNode]: FlowNodeTypeEnum.datasetSearchNode,
|
||||
[FlowTypeEnum.datasetConcatNode]: FlowNodeTypeEnum.datasetConcatNode,
|
||||
[FlowTypeEnum.answerNode]: FlowNodeTypeEnum.answerNode,
|
||||
[FlowTypeEnum.classifyQuestion]: FlowNodeTypeEnum.classifyQuestion,
|
||||
[FlowTypeEnum.contentExtract]: FlowNodeTypeEnum.contentExtract,
|
||||
[FlowTypeEnum.httpRequest468]: FlowNodeTypeEnum.httpRequest468,
|
||||
[FlowTypeEnum.runApp]: FlowNodeTypeEnum.runApp,
|
||||
[FlowTypeEnum.pluginModule]: FlowNodeTypeEnum.pluginModule,
|
||||
[FlowTypeEnum.pluginInput]: FlowNodeTypeEnum.pluginInput,
|
||||
[FlowTypeEnum.pluginOutput]: FlowNodeTypeEnum.pluginOutput,
|
||||
[FlowTypeEnum.queryExtension]: FlowNodeTypeEnum.queryExtension,
|
||||
[FlowTypeEnum.tools]: FlowNodeTypeEnum.tools,
|
||||
[FlowTypeEnum.stopTool]: FlowNodeTypeEnum.stopTool,
|
||||
[FlowTypeEnum.lafModule]: FlowNodeTypeEnum.lafModule
|
||||
};
|
||||
|
||||
const inputTypeMap: Record<any, FlowNodeInputTypeEnum> = {
|
||||
[InputTypeEnum.systemInput]: FlowNodeInputTypeEnum.input,
|
||||
[InputTypeEnum.input]: FlowNodeInputTypeEnum.input,
|
||||
[InputTypeEnum.numberInput]: FlowNodeInputTypeEnum.numberInput,
|
||||
[InputTypeEnum.select]: FlowNodeInputTypeEnum.select,
|
||||
[InputTypeEnum.target]: FlowNodeInputTypeEnum.reference,
|
||||
[InputTypeEnum.switch]: FlowNodeInputTypeEnum.switch,
|
||||
[InputTypeEnum.textarea]: FlowNodeInputTypeEnum.textarea,
|
||||
[InputTypeEnum.JSONEditor]: FlowNodeInputTypeEnum.JSONEditor,
|
||||
[InputTypeEnum.addInputParam]: FlowNodeInputTypeEnum.addInputParam,
|
||||
[InputTypeEnum.selectApp]: FlowNodeInputTypeEnum.selectApp,
|
||||
[InputTypeEnum.selectLLMModel]: FlowNodeInputTypeEnum.selectLLMModel,
|
||||
[InputTypeEnum.settingLLMModel]: FlowNodeInputTypeEnum.settingLLMModel,
|
||||
[InputTypeEnum.selectDataset]: FlowNodeInputTypeEnum.selectDataset,
|
||||
[InputTypeEnum.selectDatasetParamsModal]: FlowNodeInputTypeEnum.selectDatasetParamsModal,
|
||||
[InputTypeEnum.settingDatasetQuotePrompt]: FlowNodeInputTypeEnum.settingDatasetQuotePrompt,
|
||||
[InputTypeEnum.hidden]: FlowNodeInputTypeEnum.hidden,
|
||||
[InputTypeEnum.custom]: FlowNodeInputTypeEnum.custom
|
||||
};
|
||||
let pluginId: string | undefined = undefined;
|
||||
const inputs = node.inputs
|
||||
.map<FlowNodeInputItemType>((input) => {
|
||||
const newInput: FlowNodeInputItemType = {
|
||||
...input,
|
||||
selectedTypeIndex: 0,
|
||||
renderTypeList: inputTypeMap[input.type] ? [inputTypeMap[input.type]] : [],
|
||||
|
||||
key: input.key,
|
||||
value: input.value,
|
||||
valueType: input.valueType,
|
||||
|
||||
label: input.label,
|
||||
description: input.description,
|
||||
required: input.required,
|
||||
toolDescription: input.toolDescription,
|
||||
canEdit: input.edit,
|
||||
placeholder: input.placeholder,
|
||||
list: input.list,
|
||||
markList: input.markList,
|
||||
step: input.step,
|
||||
max: input.max,
|
||||
min: input.min,
|
||||
editField: input.editField,
|
||||
dynamicParamDefaultValue: input.defaultEditField
|
||||
? {
|
||||
inputType: input.defaultEditField.inputType
|
||||
? inputTypeMap[input.defaultEditField.inputType]
|
||||
: undefined,
|
||||
valueType: input.defaultEditField.valueType,
|
||||
required: input.defaultEditField.required
|
||||
}
|
||||
: undefined,
|
||||
llmModelType: input.llmModelType
|
||||
};
|
||||
|
||||
if (input.key === 'userChatInput') {
|
||||
newInput.label = '问题输入';
|
||||
} else if (input.key === 'quoteQA') {
|
||||
newInput.label = '';
|
||||
} else if (input.key === 'pluginId') {
|
||||
pluginId = input.value;
|
||||
}
|
||||
|
||||
return newInput;
|
||||
})
|
||||
.filter((input) => input.renderTypeList.length > 0)
|
||||
.filter((input) => {
|
||||
if (input.key === 'pluginId') {
|
||||
return false;
|
||||
}
|
||||
if (input.key === 'switch') {
|
||||
return false;
|
||||
}
|
||||
if (input.key === 'pluginStart') {
|
||||
return false;
|
||||
}
|
||||
if (input.key === 'DYNAMIC_INPUT_KEY') return;
|
||||
if (input.key === 'system_addInputParam') return;
|
||||
return true;
|
||||
});
|
||||
|
||||
const outputTypeMap: Record<any, FlowNodeOutputTypeEnum> = {
|
||||
[OutputTypeEnum.addOutputParam]: FlowNodeOutputTypeEnum.dynamic,
|
||||
[OutputTypeEnum.answer]: FlowNodeOutputTypeEnum.static,
|
||||
[OutputTypeEnum.source]: FlowNodeOutputTypeEnum.static,
|
||||
[OutputTypeEnum.hidden]: FlowNodeOutputTypeEnum.hidden
|
||||
};
|
||||
|
||||
const outputs = node.outputs
|
||||
.map<FlowNodeOutputItemType>((output) => ({
|
||||
id: output.key,
|
||||
type: output.type ? outputTypeMap[output.type] : FlowNodeOutputTypeEnum.static,
|
||||
key: output.key,
|
||||
valueType: output.valueType,
|
||||
label: output.label,
|
||||
description: output.description,
|
||||
required: output.required,
|
||||
defaultValue: output.defaultValue,
|
||||
canEdit: output.edit,
|
||||
editField: output.editField
|
||||
}))
|
||||
.filter((output) => {
|
||||
if (node.flowType === FlowTypeEnum.pluginOutput) return false;
|
||||
if (output.key === 'finish') return false;
|
||||
if (output.key === 'isEmpty') return false;
|
||||
if (output.key === 'unEmpty') return false;
|
||||
if (output.key === 'pluginStart') return false;
|
||||
if (node.flowType !== FlowTypeEnum.questionInput && output.key === 'userChatInput')
|
||||
return false;
|
||||
if (
|
||||
node.flowType === FlowTypeEnum.contentExtract &&
|
||||
(output.key === 'success' || output.key === 'failed')
|
||||
)
|
||||
return;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// special node
|
||||
if (node.flowType === FlowTypeEnum.questionInput) {
|
||||
node.name = '流程开始';
|
||||
} else if (node.flowType === FlowTypeEnum.pluginOutput) {
|
||||
node.outputs.forEach((output) => {
|
||||
inputs.push({
|
||||
key: output.key,
|
||||
valueType: output.valueType,
|
||||
renderTypeList: [FlowNodeInputTypeEnum.reference],
|
||||
label: output.key,
|
||||
canEdit: true,
|
||||
editField: {
|
||||
key: true,
|
||||
description: true,
|
||||
valueType: true
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
nodeId: node.moduleId,
|
||||
position: node.position,
|
||||
flowNodeType: nodeTypeMap[node.flowType],
|
||||
avatar: node.flowType === FlowTypeEnum.pluginModule ? node.avatar : undefined,
|
||||
name: node.name,
|
||||
intro: node.intro,
|
||||
showStatus: node.showStatus,
|
||||
pluginId,
|
||||
pluginType: node.pluginType,
|
||||
parentId: node.parentId,
|
||||
|
||||
inputs,
|
||||
outputs
|
||||
};
|
||||
});
|
||||
let newEdges: StoreEdgeItemType[] = [];
|
||||
|
||||
// 遍历output,连线
|
||||
copyNodes.forEach((node) => {
|
||||
node.outputs.forEach((output) => {
|
||||
output.targets?.forEach((target) => {
|
||||
if (output.key === 'finish') return;
|
||||
if (output.key === 'isEmpty') return;
|
||||
if (output.key === 'unEmpty') return;
|
||||
if (node.flowType !== FlowTypeEnum.questionInput && output.key === 'userChatInput') return;
|
||||
|
||||
if (output.key === NodeOutputKeyEnum.selectedTools) {
|
||||
newEdges.push({
|
||||
source: node.moduleId,
|
||||
sourceHandle: NodeOutputKeyEnum.selectedTools,
|
||||
target: target.moduleId,
|
||||
targetHandle: NodeOutputKeyEnum.selectedTools
|
||||
});
|
||||
} else if (node.flowType === FlowTypeEnum.classifyQuestion) {
|
||||
newEdges.push({
|
||||
source: node.moduleId,
|
||||
sourceHandle: getHandleId(node.moduleId, 'source', output.key),
|
||||
target: target.moduleId,
|
||||
targetHandle: getHandleId(target.moduleId, 'target', 'left')
|
||||
});
|
||||
} else if (node.flowType === FlowTypeEnum.contentExtract) {
|
||||
} else {
|
||||
newEdges.push({
|
||||
source: node.moduleId,
|
||||
sourceHandle: getHandleId(node.moduleId, 'source', 'right'),
|
||||
target: target.moduleId,
|
||||
targetHandle: getHandleId(target.moduleId, 'target', 'left')
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 去除相同source和target的线
|
||||
newEdges = newEdges.filter((edge, index, self) => {
|
||||
return (
|
||||
self.findIndex((item) => item.source === edge.source && item.target === edge.target) === index
|
||||
);
|
||||
});
|
||||
|
||||
const workflowStart = newNodes.find(
|
||||
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
|
||||
);
|
||||
|
||||
/* 更新input的取值 */
|
||||
copyNodes.forEach((node) => {
|
||||
node.outputs.forEach((output) => {
|
||||
output.targets?.forEach((target) => {
|
||||
const targetNode = newNodes.find((item) => item.nodeId === target.moduleId);
|
||||
if (!targetNode) return;
|
||||
|
||||
const targetInput = targetNode.inputs.find((item) => item.key === target.key);
|
||||
if (!targetInput) return;
|
||||
|
||||
targetInput.value = [node.moduleId, output.key];
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 更新特殊的输入(输入全部从开始取)
|
||||
newNodes.forEach((node) => {
|
||||
node.inputs.forEach((input) => {
|
||||
if (workflowStart && input.key === NodeInputKeyEnum.userChatInput) {
|
||||
input.value = [workflowStart.nodeId, NodeOutputKeyEnum.userChatInput];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log({
|
||||
nodes: newNodes.filter((node) => node.nodeId),
|
||||
edges: newEdges
|
||||
});
|
||||
return {
|
||||
nodes: newNodes.filter((node) => node.nodeId),
|
||||
edges: newEdges
|
||||
};
|
||||
};
|
8
projects/app/src/web/core/workflow/api.ts
Normal file
8
projects/app/src/web/core/workflow/api.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { GET, POST, PUT, DELETE } from '@/web/common/api/request';
|
||||
import { PostWorkflowDebugProps, PostWorkflowDebugResponse } from '@/global/core/workflow/api';
|
||||
|
||||
export const postWorkflowDebug = (data: PostWorkflowDebugProps) =>
|
||||
POST<PostWorkflowDebugResponse>('/core/workflow/debug', {
|
||||
...data,
|
||||
mode: 'debug'
|
||||
});
|
@@ -1,47 +1,47 @@
|
||||
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
|
||||
export const FlowValueTypeMap = {
|
||||
[ModuleIOValueTypeEnum.string]: {
|
||||
[WorkflowIOValueTypeEnum.string]: {
|
||||
handlerStyle: {
|
||||
borderColor: '#36ADEF'
|
||||
},
|
||||
label: 'core.module.valueType.string',
|
||||
value: ModuleIOValueTypeEnum.string,
|
||||
value: WorkflowIOValueTypeEnum.string,
|
||||
description: ''
|
||||
},
|
||||
[ModuleIOValueTypeEnum.number]: {
|
||||
[WorkflowIOValueTypeEnum.number]: {
|
||||
handlerStyle: {
|
||||
borderColor: '#FB7C3C'
|
||||
},
|
||||
label: 'core.module.valueType.number',
|
||||
value: ModuleIOValueTypeEnum.number,
|
||||
value: WorkflowIOValueTypeEnum.number,
|
||||
description: ''
|
||||
},
|
||||
[ModuleIOValueTypeEnum.boolean]: {
|
||||
[WorkflowIOValueTypeEnum.boolean]: {
|
||||
handlerStyle: {
|
||||
borderColor: '#E7D118'
|
||||
},
|
||||
label: 'core.module.valueType.boolean',
|
||||
value: ModuleIOValueTypeEnum.boolean,
|
||||
value: WorkflowIOValueTypeEnum.boolean,
|
||||
description: ''
|
||||
},
|
||||
[ModuleIOValueTypeEnum.chatHistory]: {
|
||||
[WorkflowIOValueTypeEnum.chatHistory]: {
|
||||
handlerStyle: {
|
||||
borderColor: '#00A9A6'
|
||||
},
|
||||
label: 'core.module.valueType.chatHistory',
|
||||
value: ModuleIOValueTypeEnum.chatHistory,
|
||||
value: WorkflowIOValueTypeEnum.chatHistory,
|
||||
description: `{
|
||||
obj: System | Human | AI;
|
||||
value: string;
|
||||
}[]`
|
||||
},
|
||||
[ModuleIOValueTypeEnum.datasetQuote]: {
|
||||
[WorkflowIOValueTypeEnum.datasetQuote]: {
|
||||
handlerStyle: {
|
||||
borderColor: '#A558C9'
|
||||
},
|
||||
label: 'core.module.valueType.datasetQuote',
|
||||
value: ModuleIOValueTypeEnum.datasetQuote,
|
||||
value: WorkflowIOValueTypeEnum.datasetQuote,
|
||||
description: `{
|
||||
id: string;
|
||||
datasetId: string;
|
||||
@@ -52,36 +52,44 @@ export const FlowValueTypeMap = {
|
||||
a: string
|
||||
}[]`
|
||||
},
|
||||
[ModuleIOValueTypeEnum.any]: {
|
||||
[WorkflowIOValueTypeEnum.any]: {
|
||||
handlerStyle: {
|
||||
borderColor: '#9CA2A8'
|
||||
},
|
||||
label: 'core.module.valueType.any',
|
||||
value: ModuleIOValueTypeEnum.any,
|
||||
value: WorkflowIOValueTypeEnum.any,
|
||||
description: ''
|
||||
},
|
||||
[ModuleIOValueTypeEnum.selectApp]: {
|
||||
[WorkflowIOValueTypeEnum.selectApp]: {
|
||||
handlerStyle: {
|
||||
borderColor: '#6a6efa'
|
||||
},
|
||||
label: 'core.module.valueType.selectApp',
|
||||
value: ModuleIOValueTypeEnum.selectApp,
|
||||
value: WorkflowIOValueTypeEnum.selectApp,
|
||||
description: ''
|
||||
},
|
||||
[ModuleIOValueTypeEnum.selectDataset]: {
|
||||
[WorkflowIOValueTypeEnum.selectDataset]: {
|
||||
handlerStyle: {
|
||||
borderColor: '#21ba45'
|
||||
},
|
||||
label: 'core.module.valueType.selectDataset',
|
||||
value: ModuleIOValueTypeEnum.selectDataset,
|
||||
value: WorkflowIOValueTypeEnum.selectDataset,
|
||||
description: ''
|
||||
},
|
||||
[ModuleIOValueTypeEnum.tools]: {
|
||||
[WorkflowIOValueTypeEnum.tools]: {
|
||||
handlerStyle: {
|
||||
borderColor: '#21ba45'
|
||||
},
|
||||
label: 'core.module.valueType.tools',
|
||||
value: ModuleIOValueTypeEnum.tools,
|
||||
value: WorkflowIOValueTypeEnum.tools,
|
||||
description: ''
|
||||
},
|
||||
[WorkflowIOValueTypeEnum.dynamic]: {
|
||||
handlerStyle: {
|
||||
borderColor: '#9CA2A8'
|
||||
},
|
||||
label: '动态数据',
|
||||
value: WorkflowIOValueTypeEnum.any,
|
||||
description: ''
|
||||
}
|
||||
};
|
||||
|
1
projects/app/src/web/core/workflow/constants/index.ts
Normal file
1
projects/app/src/web/core/workflow/constants/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const VARIABLE_NODE_ID = 'VARIABLE_NODE_ID';
|
@@ -1,12 +1,10 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { FlowNodeTemplateType } from '@fastgpt/global/core/module/type';
|
||||
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import { getTeamPlugTemplates, getSystemPlugTemplates } from '../../plugin/api';
|
||||
|
||||
type State = {
|
||||
basicNodeTemplates: FlowNodeTemplateType[];
|
||||
setBasicNodeTemplates: (basicNodeTemplates: FlowNodeTemplateType[]) => void;
|
||||
systemNodeTemplates: FlowNodeTemplateType[];
|
||||
loadSystemNodeTemplates: (init?: boolean) => Promise<FlowNodeTemplateType[]>;
|
||||
teamPluginNodeTemplates: FlowNodeTemplateType[];
|
||||
@@ -21,12 +19,6 @@ export const useWorkflowStore = create<State>()(
|
||||
devtools(
|
||||
persist(
|
||||
immer((set, get) => ({
|
||||
basicNodeTemplates: [],
|
||||
setBasicNodeTemplates: (basicNodeTemplates) => {
|
||||
set((state) => {
|
||||
state.basicNodeTemplates = basicNodeTemplates;
|
||||
});
|
||||
},
|
||||
systemNodeTemplates: [],
|
||||
async loadSystemNodeTemplates(init) {
|
||||
if (!init && get().systemNodeTemplates.length > 0) {
|
||||
|
234
projects/app/src/web/core/workflow/utils.ts
Normal file
234
projects/app/src/web/core/workflow/utils.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import type {
|
||||
StoreNodeItemType,
|
||||
FlowNodeItemType,
|
||||
FlowNodeTemplateType
|
||||
} from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import type { Edge, Node, XYPosition } from 'reactflow';
|
||||
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
|
||||
import {
|
||||
EDGE_TYPE,
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowNodeTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { EmptyNode } from '@fastgpt/global/core/workflow/template/system/emptyNode';
|
||||
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { systemConfigNode2VariableNode } from './adapt';
|
||||
import { VARIABLE_NODE_ID } from './constants';
|
||||
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
|
||||
export const nodeTemplate2FlowNode = ({
|
||||
template,
|
||||
position,
|
||||
selected
|
||||
}: {
|
||||
template: FlowNodeTemplateType;
|
||||
position: XYPosition;
|
||||
selected?: boolean;
|
||||
}): Node<FlowNodeItemType> => {
|
||||
// replace item data
|
||||
const moduleItem: FlowNodeItemType = {
|
||||
...template,
|
||||
nodeId: getNanoid()
|
||||
};
|
||||
|
||||
return {
|
||||
id: moduleItem.nodeId,
|
||||
type: moduleItem.flowNodeType,
|
||||
data: moduleItem,
|
||||
position: position,
|
||||
selected
|
||||
};
|
||||
};
|
||||
export const storeNode2FlowNode = ({
|
||||
item: storeNode
|
||||
}: {
|
||||
item: StoreNodeItemType;
|
||||
}): Node<FlowNodeItemType> => {
|
||||
// init some static data
|
||||
const template =
|
||||
moduleTemplatesFlat.find((template) => template.flowNodeType === storeNode.flowNodeType) ||
|
||||
EmptyNode;
|
||||
|
||||
// replace item data
|
||||
const moduleItem: FlowNodeItemType = {
|
||||
...template,
|
||||
...storeNode,
|
||||
avatar: storeNode?.avatar || template?.avatar,
|
||||
inputs: storeNode.inputs
|
||||
.map((storeInput) => {
|
||||
const templateInput =
|
||||
template.inputs.find((item) => item.key === storeInput.key) || storeInput;
|
||||
return {
|
||||
...templateInput,
|
||||
...storeInput,
|
||||
renderTypeList: templateInput.renderTypeList
|
||||
};
|
||||
})
|
||||
.concat(
|
||||
template.inputs.filter((item) => !storeNode.inputs.some((input) => input.key === item.key))
|
||||
),
|
||||
outputs: storeNode.outputs.map((storeOutput) => {
|
||||
const templateOutput =
|
||||
template.outputs.find((item) => item.key === storeOutput.key) || storeOutput;
|
||||
return {
|
||||
...storeOutput,
|
||||
...templateOutput,
|
||||
value: storeOutput.value
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
return {
|
||||
id: storeNode.nodeId,
|
||||
type: storeNode.flowNodeType,
|
||||
data: moduleItem,
|
||||
position: storeNode.position || { x: 0, y: 0 }
|
||||
};
|
||||
};
|
||||
export const storeEdgesRenderEdge = ({ edge }: { edge: StoreEdgeItemType }) => {
|
||||
return {
|
||||
...edge,
|
||||
id: getNanoid(),
|
||||
type: EDGE_TYPE
|
||||
};
|
||||
};
|
||||
|
||||
export const computedNodeInputReference = ({
|
||||
nodeId,
|
||||
nodes,
|
||||
edges
|
||||
}: {
|
||||
nodeId: string;
|
||||
nodes: FlowNodeItemType[];
|
||||
edges: Edge[];
|
||||
}) => {
|
||||
// get current node
|
||||
const node = nodes.find((item) => item.nodeId === nodeId);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
let sourceNodes: FlowNodeItemType[] = [];
|
||||
// 根据 edge 获取所有的 source 节点(source节点会继续向前递归获取)
|
||||
const findSourceNode = (nodeId: string) => {
|
||||
const targetEdges = edges.filter((item) => item.target === nodeId);
|
||||
targetEdges.forEach((edge) => {
|
||||
const sourceNode = nodes.find((item) => item.nodeId === edge.source);
|
||||
if (!sourceNode) return;
|
||||
|
||||
// 去重
|
||||
if (sourceNodes.some((item) => item.nodeId === sourceNode.nodeId)) {
|
||||
return;
|
||||
}
|
||||
sourceNodes.push(sourceNode);
|
||||
findSourceNode(sourceNode.nodeId);
|
||||
});
|
||||
};
|
||||
findSourceNode(nodeId);
|
||||
|
||||
// add system config node
|
||||
const systemConfigNode = nodes.find(
|
||||
(item) => item.flowNodeType === FlowNodeTypeEnum.systemConfig
|
||||
);
|
||||
|
||||
if (systemConfigNode) {
|
||||
sourceNodes.unshift(systemConfigNode2VariableNode(systemConfigNode));
|
||||
}
|
||||
|
||||
return sourceNodes;
|
||||
};
|
||||
|
||||
/* Connection rules */
|
||||
export const checkWorkflowNodeAndConnection = ({
|
||||
nodes,
|
||||
edges
|
||||
}: {
|
||||
nodes: Node<FlowNodeItemType, string | undefined>[];
|
||||
edges: Edge<any>[];
|
||||
}): string[] | undefined => {
|
||||
// 1. reference check. Required value
|
||||
for (const node of nodes) {
|
||||
const data = node.data;
|
||||
const inputs = data.inputs;
|
||||
const isToolNode = edges.some(
|
||||
(edge) =>
|
||||
edge.targetHandle === NodeOutputKeyEnum.selectedTools && edge.target === node.data.nodeId
|
||||
);
|
||||
|
||||
if (
|
||||
data.flowNodeType === FlowNodeTypeEnum.systemConfig ||
|
||||
data.flowNodeType === FlowNodeTypeEnum.pluginInput ||
|
||||
data.flowNodeType === FlowNodeTypeEnum.pluginOutput ||
|
||||
data.flowNodeType === FlowNodeTypeEnum.workflowStart
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check node input
|
||||
if (
|
||||
inputs.some((input) => {
|
||||
// check is tool input
|
||||
if (isToolNode && input.toolDescription) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (input.required) {
|
||||
if (Array.isArray(input.value) && input.value.length === 0) return true;
|
||||
if (input.value === undefined) return true;
|
||||
}
|
||||
|
||||
// check reference invalid
|
||||
const renderType = input.renderTypeList[input.selectedTypeIndex || 0];
|
||||
if (renderType === FlowNodeInputTypeEnum.reference && input.required) {
|
||||
if (!input.value || !Array.isArray(input.value) || input.value.length !== 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// variable key not need to check
|
||||
if (input.value[0] === VARIABLE_NODE_ID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Can not find key
|
||||
const sourceNode = nodes.find((item) => item.data.nodeId === input.value[0]);
|
||||
if (!sourceNode) {
|
||||
return true;
|
||||
}
|
||||
const sourceOutput = sourceNode.data.outputs.find((item) => item.id === input.value[1]);
|
||||
if (!sourceOutput) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
) {
|
||||
return [data.nodeId];
|
||||
}
|
||||
|
||||
// check empty node(not edge)
|
||||
const hasEdge = edges.some(
|
||||
(edge) => edge.source === data.nodeId || edge.target === data.nodeId
|
||||
);
|
||||
if (!hasEdge) {
|
||||
return [data.nodeId];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const filterSensitiveNodesData = (nodes: StoreNodeItemType[]) => {
|
||||
const cloneNodes = JSON.parse(JSON.stringify(nodes)) as StoreNodeItemType[];
|
||||
|
||||
cloneNodes.forEach((node) => {
|
||||
// selected dataset
|
||||
if (node.flowNodeType === FlowNodeTypeEnum.datasetSearchNode) {
|
||||
node.inputs.forEach((input) => {
|
||||
if (input.key === NodeInputKeyEnum.datasetSelectList) {
|
||||
input.value = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
return cloneNodes;
|
||||
};
|
@@ -1,4 +1,13 @@
|
||||
.react-flow__panel.react-flow__attribution {
|
||||
z-index: 0;
|
||||
left: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.react-flow__handle {
|
||||
&.connecting {
|
||||
border-color: #039855 !important;
|
||||
& .flow-handle {
|
||||
border-color: #039855 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user