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

@@ -7,17 +7,22 @@ import { useCopyData } from '@/web/common/hooks/useCopyData';
import dynamic from 'next/dynamic';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@/components/MyTooltip';
import { getFlowStore } from '@/components/core/module/Flow/FlowProvider';
import { filterExportModules, flowNode2Modules } from '@/components/core/module/utils';
import { filterExportModules, flowNode2StoreNodes } from '@/components/core/workflow/utils';
import { putUpdatePlugin } from '@/web/core/plugin/api';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleItemType } from '@fastgpt/global/core/module/type';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
import MyMenu from '@/components/MyMenu';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import {
getWorkflowStore,
useFlowProviderStore
} from '@/components/core/workflow/Flow/FlowProvider';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import {
checkWorkflowNodeAndConnection,
filterSensitiveNodesData
} from '@/web/core/workflow/utils';
const ImportSettings = dynamic(() => import('@/components/core/module/Flow/ImportSettings'));
const PreviewPlugin = dynamic(() => import('./Preview'));
const ImportSettings = dynamic(() => import('@/components/core/workflow/Flow/ImportSettings'));
type Props = { plugin: PluginItemSchema; onClose: () => void };
@@ -26,92 +31,31 @@ const Header = ({ plugin, onClose }: Props) => {
const { t } = useTranslation();
const { toast } = useToast();
const { copyData } = useCopyData();
const { edges, onUpdateNodeError } = useFlowProviderStore();
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
const [previewModules, setPreviewModules] = React.useState<ModuleItemType[]>();
const flow2ModulesAndCheck = useCallback(async () => {
const { nodes, edges } = await getFlowStore();
const flowData2StoreDataAndCheck = useCallback(async () => {
const { nodes } = await getWorkflowStore();
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
if (!checkResults) {
const storeNodes = flowNode2StoreNodes({ nodes, edges });
const modules = flowNode2Modules({ nodes, edges });
// check required connect
for (let i = 0; i < modules.length; i++) {
const item = modules[i];
// update custom input connected
if (item.flowType === FlowNodeTypeEnum.pluginInput) {
item.inputs.forEach((item) => {
item.connected = true;
});
if (
item.outputs.find(
(output) =>
output.key !== ModuleOutputKeyEnum.pluginStart && output.targets.length === 0
)
) {
toast({
status: 'warning',
title: t('module.Plugin input must connect')
});
return false;
}
}
if (
item.flowType === FlowNodeTypeEnum.pluginOutput &&
item.inputs.find((input) => !input.connected)
) {
toast({
status: 'warning',
title: t('core.module.Plugin output must connect')
});
return false;
}
if (
item.inputs.find((input) => {
if (!input.required || input.connected) return false;
if (input.value === undefined || input.value === '' || input.value?.length === 0) {
return true;
}
return false;
})
) {
toast({
status: 'warning',
title: `${item.name}】存在未填或未连接参数`
});
return false;
}
}
// plugin must have input
const pluginInputModule = modules.find(
(item) => item.flowType === FlowNodeTypeEnum.pluginInput
);
if (!pluginInputModule) {
return storeNodes;
} else {
checkResults.forEach((nodeId) => onUpdateNodeError(nodeId, true));
toast({
status: 'warning',
title: t('module.Plugin input is required')
title: t('core.workflow.Check Failed')
});
return false;
}
if (pluginInputModule.inputs.length < 1) {
toast({
status: 'warning',
title: t('module.Plugin input is not value')
});
return false;
}
return modules;
}, [t, toast]);
}, [edges, onUpdateNodeError, t, toast]);
const { mutate: onclickSave, isLoading } = useRequest({
mutationFn: (modules: ModuleItemType[]) => {
mutationFn: ({ nodes, edges }: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => {
return putUpdatePlugin({
id: plugin._id,
modules
modules: nodes,
edges
});
},
successToast: '保存配置成功',
@@ -158,15 +102,25 @@ const Header = ({ plugin, onClose }: Props) => {
label: t('app.Export Configs'),
icon: 'export',
onClick: async () => {
const modules = await flow2ModulesAndCheck();
if (modules) {
copyData(filterExportModules(modules), t('app.Export Config Successful'));
const data = await flowData2StoreDataAndCheck();
if (data) {
copyData(
JSON.stringify(
{
nodes: filterSensitiveNodesData(data.nodes),
edges: data.edges
},
null,
2
),
t('app.Export Config Successful')
);
}
}
}
]}
/>
<MyTooltip label={t('module.Preview Plugin')}>
{/* <MyTooltip label={t('module.Preview Plugin')}>
<IconButton
mr={[3, 5]}
icon={<MyIcon name={'core/modules/previewLight'} w={['14px', '16px']} />}
@@ -174,19 +128,19 @@ const Header = ({ plugin, onClose }: Props) => {
aria-label={'save'}
variant={'whitePrimary'}
onClick={async () => {
const modules = await flow2ModulesAndCheck();
const modules = await flowData2StoreDataAndCheck();
if (modules) {
setPreviewModules(modules);
}
}}
/>
</MyTooltip>
</MyTooltip> */}
<Button
size={'sm'}
isLoading={isLoading}
leftIcon={<MyIcon name={'common/saveFill'} w={['14px', '16px']} />}
onClick={async () => {
const modules = await flow2ModulesAndCheck();
const modules = await flowData2StoreDataAndCheck();
if (modules) {
onclickSave(modules);
}
@@ -196,13 +150,6 @@ const Header = ({ plugin, onClose }: Props) => {
</Button>
</Flex>
{isOpenImport && <ImportSettings onClose={onCloseImport} />}
{!!previewModules && (
<PreviewPlugin
plugin={plugin}
modules={previewModules}
onClose={() => setPreviewModules(undefined)}
/>
)}
</>
);
};

View File

@@ -1,73 +0,0 @@
import React, { useEffect } from 'react';
import ReactFlow, { Background, ReactFlowProvider, useNodesState } from 'reactflow';
import { FlowModuleItemType, ModuleItemType } from '@fastgpt/global/core/module/type';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import dynamic from 'next/dynamic';
import { plugin2ModuleIO } from '@fastgpt/global/core/module/utils';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { PluginItemSchema } from '@fastgpt/global/core/plugin/type';
import { appModule2FlowNode } from '@/utils/adapt';
const nodeTypes = {
[FlowNodeTypeEnum.pluginModule]: dynamic(
() => import('@/components/core/module/Flow/components/nodes/NodeSimple')
)
};
const PreviewPlugin = ({
plugin,
modules,
onClose
}: {
plugin: PluginItemSchema;
modules: ModuleItemType[];
onClose: () => void;
}) => {
const { t } = useTranslation();
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowModuleItemType>([]);
useEffect(() => {
setNodes([
appModule2FlowNode({
item: {
moduleId: 'plugin',
flowType: FlowNodeTypeEnum.pluginModule,
avatar: plugin.avatar,
name: plugin.name,
intro: plugin.intro,
...plugin2ModuleIO(plugin._id, modules)
}
})
]);
}, [modules, plugin, setNodes]);
return (
<MyModal
isOpen
title={t('module.Preview Plugin')}
iconSrc="/imgs/modal/preview.svg"
onClose={onClose}
isCentered
>
<Box h={'400px'} w={'400px'}>
<ReactFlowProvider>
<ReactFlow
fitView
nodes={nodes}
edges={[]}
minZoom={0.1}
maxZoom={1.5}
nodeTypes={nodeTypes}
onNodesChange={onNodesChange}
>
<Background />
</ReactFlow>
</ReactFlowProvider>
</Box>
</MyModal>
);
};
export default React.memo(PreviewPlugin);

View File

@@ -1,11 +1,9 @@
import React, { useEffect, useMemo } from 'react';
import React, { useEffect } from 'react';
import { useRouter } from 'next/router';
import Header from './Header';
import Flow from '@/components/core/module/Flow';
import FlowProvider, { useFlowProviderStore } from '@/components/core/module/Flow/FlowProvider';
import { FlowNodeTemplateType } from '@fastgpt/global/core/module/type.d';
import { pluginSystemModuleTemplates } from '@fastgpt/global/core/module/template/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import Flow from '@/components/core/workflow/Flow';
import FlowProvider, { useFlowProviderStore } from '@/components/core/workflow/Flow/FlowProvider';
import { pluginSystemModuleTemplates } from '@fastgpt/global/core/workflow/template/constants';
import { serviceSideProps } from '@/web/common/utils/i18n';
import { useQuery } from '@tanstack/react-query';
import { getOnePlugin } from '@/web/core/plugin/api';
@@ -13,7 +11,8 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
import Loading from '@fastgpt/web/components/common/MyLoading';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useTranslation } from 'next-i18next';
import { useWorkflowStore } from '@/web/core/workflow/store/workflow';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
type Props = { pluginId: string };
@@ -21,8 +20,7 @@ const Render = ({ pluginId }: Props) => {
const { t } = useTranslation();
const router = useRouter();
const { toast } = useToast();
const { nodes, initData } = useFlowProviderStore();
const { setBasicNodeTemplates } = useWorkflowStore();
const { initData } = useFlowProviderStore();
const { data: pluginDetail } = useQuery(
['getOnePlugin', pluginId],
@@ -37,43 +35,39 @@ const Render = ({ pluginId }: Props) => {
}
}
);
const isV2Workflow = pluginDetail?.version === 'v2';
const { openConfirm, ConfirmModal } = useConfirm({
showCancel: false,
content:
'检测到您的高级编排为旧版,系统将为您自动格式化成新版工作流。\n\n由于版本差异较大会导致许多工作流无法正常排布请重新手动连接工作流。如仍异常可尝试删除对应节点后重新添加。\n\n你可以直接点击测试进行调试无需点击保存点击保存为新版工作流。'
});
useEffect(() => {
initData(JSON.parse(JSON.stringify(pluginDetail?.modules || [])));
}, [pluginDetail?.modules]);
if (isV2Workflow) {
initData(
JSON.parse(
JSON.stringify({
nodes: pluginDetail?.modules || [],
edges: pluginDetail?.edges || []
})
)
);
}
}, [isV2Workflow, pluginDetail?.edges, pluginDetail?.modules]);
useEffect(() => {
const concatTemplates = [...pluginSystemModuleTemplates];
const copyTemplates: FlowNodeTemplateType[] = JSON.parse(JSON.stringify(concatTemplates));
const filterType: Record<string, 1> = {
[FlowNodeTypeEnum.userGuide]: 1,
[FlowNodeTypeEnum.pluginInput]: 1,
[FlowNodeTypeEnum.pluginOutput]: 1
};
// filter some template
nodes.forEach((node) => {
if (node.type && filterType[node.type]) {
copyTemplates.forEach((module, index) => {
if (module.flowType === node.type) {
copyTemplates.splice(index, 1);
}
});
}
});
// filter hideInPlugin inputs
copyTemplates.forEach((template) => {
template.inputs = template.inputs.filter((input) => !input.hideInPlugin);
});
setBasicNodeTemplates(copyTemplates);
}, [nodes, setBasicNodeTemplates]);
if (!isV2Workflow && pluginDetail) {
openConfirm(() => {
initData(JSON.parse(JSON.stringify(v1Workflow2V2((pluginDetail.modules || []) as any))));
})();
}
}, [isV2Workflow, openConfirm, pluginDetail]);
return pluginDetail ? (
<Flow Header={<Header plugin={pluginDetail} onClose={() => router.back()} />} />
<>
<Flow Header={<Header plugin={pluginDetail} onClose={() => router.back()} />} />
{!isV2Workflow && <ConfirmModal countDown={0} />}
</>
) : (
<Loading />
);
@@ -81,7 +75,7 @@ const Render = ({ pluginId }: Props) => {
export default function FlowEdit(props: any) {
return (
<FlowProvider mode={'plugin'}>
<FlowProvider mode={'plugin'} basicNodeTemplates={pluginSystemModuleTemplates}>
<Render {...props} />
</FlowProvider>
);

View File

@@ -30,10 +30,10 @@ export const defaultForm: EditFormType = {
type: PluginTypeEnum.custom,
modules: [
{
moduleId: nanoid(),
nodeId: nanoid(),
name: '定义插件输入',
avatar: '/imgs/module/input.png',
flowType: 'pluginInput',
avatar: '/imgs/workflow/input.png',
flowNodeType: 'pluginInput',
showStatus: false,
position: {
x: 616.4226348688949,
@@ -43,10 +43,10 @@ export const defaultForm: EditFormType = {
outputs: []
},
{
moduleId: nanoid(),
nodeId: nanoid(),
name: '定义插件输出',
avatar: '/imgs/module/output.png',
flowType: 'pluginOutput',
avatar: '/imgs/workflow/output.png',
flowNodeType: 'pluginOutput',
showStatus: false,
position: {
x: 1607.7142331269126,

View File

@@ -40,7 +40,7 @@ import MyModal from '@fastgpt/web/components/common/MyModal';
import { EditFormType } from './type';
import { FolderImgUrl } from '@fastgpt/global/common/file/image/constants';
import HttpInput from '@fastgpt/web/components/common/Input/HttpInput';
import { HttpHeaders } from '@/components/core/module/Flow/components/nodes/NodeHttp';
import { HttpHeaders } from '@/components/core/workflow/Flow/nodes/NodeHttp';
import { OpenApiJsonSchema } from '@fastgpt/global/core/plugin/httpPlugin/type';
export const defaultHttpPlugin: CreateOnePluginParams = {
@@ -150,7 +150,7 @@ const HttpPluginEditModal = ({
[setValue, t, toast]
);
const { mutate: onclickDelPlugin, isLoading: isDeleting } = useRequest({
const { mutate: onClickDelPlugin, isLoading: isDeleting } = useRequest({
mutationFn: async () => {
if (!defaultPlugin.id) return;
@@ -209,7 +209,7 @@ const HttpPluginEditModal = ({
<MyModal
isOpen
onClose={onClose}
iconSrc="/imgs/module/http.png"
iconSrc="/imgs/workflow/http.png"
title={isEdit ? t('plugin.Edit Http Plugin') : t('plugin.Import Plugin')}
w={['90vw', '600px']}
h={['90vh', '80vh']}
@@ -512,7 +512,7 @@ const HttpPluginEditModal = ({
isLoading={isDeleting}
onClick={(e) => {
e.stopPropagation();
openConfirm(onclickDelPlugin)();
openConfirm(onClickDelPlugin)();
}}
/>
)}

View File

@@ -11,13 +11,16 @@ import PageContainer from '@/components/PageContainer';
import Avatar from '@/components/Avatar';
import EditModal, { defaultForm } from './component/EditModal';
import { getPluginPaths, getUserPlugins } from '@/web/core/plugin/api';
import EmptyTip from '@/components/EmptyTip';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useUserStore } from '@/web/support/user/useUserStore';
import MyMenu from '@/components/MyMenu';
import HttpPluginEditModal, { defaultHttpPlugin } from './component/HttpPluginEditModal';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { defaultHttpPlugin } from './component/HttpPluginEditModal';
import { PluginTypeEnum } from '@fastgpt/global/core/plugin/constants';
import ParentPaths from '@/components/common/ParentPaths';
import { EditFormType } from './component/type';
import dynamic from 'next/dynamic';
const HttpPluginEditModal = dynamic(() => import('./component/HttpPluginEditModal'));
const TeamPlugins = () => {
const { t } = useTranslation();
@@ -58,7 +61,7 @@ const TeamPlugins = () => {
}))}
FirstPathDom={
<Flex flex={1} alignItems={'center'}>
<Image src={'/imgs/module/plugin.svg'} alt={''} mr={2} h={'24px'} />
<Image src={'/imgs/workflow/plugin.svg'} alt={''} mr={2} h={'24px'} />
<Box className="textlg" letterSpacing={1} fontSize={'24px'} fontWeight={'bold'}>
{t('plugin.My Plugins')}({t('common.Beta')})
</Box>
@@ -88,7 +91,7 @@ const TeamPlugins = () => {
{
label: (
<Flex>
<Image src={'/imgs/module/plugin.svg'} alt={''} w={'18px'} mr={1} />
<Image src={'/imgs/workflow/plugin.svg'} alt={''} w={'18px'} mr={1} />
{t('plugin.Custom Plugin')}
</Flex>
),
@@ -97,7 +100,7 @@ const TeamPlugins = () => {
{
label: (
<Flex display={'flex'} alignItems={'center'}>
<Image src={'/imgs/module/http.png'} alt={''} w={'18px'} h={'14px'} mr={1} />
<Image src={'/imgs/workflow/http.png'} alt={''} w={'18px'} h={'14px'} mr={1} />
{t('plugin.HTTP Plugin')}
</Flex>
),