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:
Archer
2024-04-25 17:51:20 +08:00
committed by GitHub
parent b08d81f887
commit 439c819ff1
505 changed files with 23570 additions and 18215 deletions

View File

@@ -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',

View File

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

View File

@@ -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')

View File

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

View File

@@ -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

View File

@@ -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 || ''

View File

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

View 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
};
};

View 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'
});

View File

@@ -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: ''
}
};

View File

@@ -0,0 +1 @@
export const VARIABLE_NODE_ID = 'VARIABLE_NODE_ID';

View File

@@ -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) {

View 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;
};

View File

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