diff --git a/packages/global/core/plugin/type.d.ts b/packages/global/core/plugin/type.d.ts index 3de9ff110..7d293862b 100644 --- a/packages/global/core/plugin/type.d.ts +++ b/packages/global/core/plugin/type.d.ts @@ -23,6 +23,7 @@ export type PluginItemSchema = { customHeaders?: string; }; version?: 'v1' | 'v2'; + nodeVersion?: string; }; /* plugin template */ @@ -32,6 +33,7 @@ export type PluginTemplateType = PluginRuntimeType & { source: `${PluginSourceEnum}`; templateType: FlowNodeTemplateType['templateType']; intro: string; + nodeVersion: string; }; export type PluginRuntimeType = { diff --git a/packages/global/core/workflow/type/index.d.ts b/packages/global/core/workflow/type/index.d.ts index 2862a6269..9e50458fa 100644 --- a/packages/global/core/workflow/type/index.d.ts +++ b/packages/global/core/workflow/type/index.d.ts @@ -64,6 +64,7 @@ export type FlowNodeTemplateType = FlowNodeCommonType & { // action forbidDelete?: boolean; // forbid delete unique?: boolean; + nodeVersion?: string; }; export type FlowNodeItemType = FlowNodeTemplateType & { nodeId: string; diff --git a/packages/service/core/plugin/controller.ts b/packages/service/core/plugin/controller.ts index a2718cd35..fb31b3916 100644 --- a/packages/service/core/plugin/controller.ts +++ b/packages/service/core/plugin/controller.ts @@ -52,7 +52,8 @@ const getPluginTemplateById = async (id: string): Promise => nodes: item.modules, edges: item.edges, templateType: FlowNodeTemplateTypeEnum.personalPlugin, - isTool: true + isTool: true, + nodeVersion: item?.nodeVersion || '' }; } return Promise.reject('plugin not found'); @@ -72,6 +73,7 @@ export async function getPluginPreviewNode({ id }: { id: string }): Promise { return JSON.stringify(value); } if (type === 'number') return Number(value); - if (type === 'boolean') return Boolean(value); + if (type === 'boolean') return value === 'true' ? true : false; try { if (type === WorkflowIOValueTypeEnum.datasetQuote && !Array.isArray(value)) { return JSON.parse(value); diff --git a/projects/app/i18n/en/app.json b/projects/app/i18n/en/app.json index 8d5b17b03..22b8ab292 100644 --- a/projects/app/i18n/en/app.json +++ b/projects/app/i18n/en/app.json @@ -39,6 +39,7 @@ }, "module": { "Combine Modules": "Combine Modules", + "Confirm Sync": "Using the latest template will overwrite the existing one and may result in the loss of some previous configuration information. Please confirm.", "Custom Title Tip": "This title will be displayed during the conversation", "My Modules": "My Modules", "No Modules": "No modules yet~", diff --git a/projects/app/i18n/zh/app.json b/projects/app/i18n/zh/app.json index 93205c1d4..83c85cec3 100644 --- a/projects/app/i18n/zh/app.json +++ b/projects/app/i18n/zh/app.json @@ -38,6 +38,7 @@ }, "module": { "Combine Modules": "组合模块", + "Confirm Sync": "将会使用最新模板进行覆盖,可能会丢失一些旧的配置信息,请确认", "Custom Title Tip": "该标题名字会展示在对话过程中", "My Modules": "", "No Modules": "还没有模块~", diff --git a/projects/app/src/components/core/workflow/Flow/nodes/render/NodeCard.tsx b/projects/app/src/components/core/workflow/Flow/nodes/render/NodeCard.tsx index aabf1a0d5..efd7c7237 100644 --- a/projects/app/src/components/core/workflow/Flow/nodes/render/NodeCard.tsx +++ b/projects/app/src/components/core/workflow/Flow/nodes/render/NodeCard.tsx @@ -1,13 +1,15 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Box, Button, Card, Flex } from '@chakra-ui/react'; import MyIcon from '@fastgpt/web/components/common/Icon'; import Avatar from '@/components/Avatar'; -import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d'; +import type { + FlowNodeItemType, + FlowNodeTemplateType +} from '@fastgpt/global/core/workflow/type/index.d'; import { useTranslation } from 'next-i18next'; import { useEditTitle } from '@/web/common/hooks/useEditTitle'; import { useToast } from '@fastgpt/web/hooks/useToast'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; -import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { LOGO_ICON } from '@fastgpt/global/common/system/constants'; import { ToolTargetHandle } from './Handle/ToolHandle'; @@ -17,7 +19,6 @@ import { useDebug } from '../../hooks/useDebug'; import { ResponseBox } from '@/components/ChatBox/WholeResponseModal'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; import { getPreviewPluginModule } from '@/web/core/plugin/api'; -import { getErrText } from '@fastgpt/global/common/error/utils'; import { storeNode2FlowNode, updateFlowNodeVersion } from '@/web/core/workflow/utils'; import { getNanoid } from '@fastgpt/global/common/string/tools'; import { useContextSelector } from 'use-context-selector'; @@ -26,6 +27,8 @@ import { useI18n } from '@/web/context/I18n'; import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants'; import { QuestionOutlineIcon } from '@chakra-ui/icons'; import MyTooltip from '@/components/MyTooltip'; +import { isEqual } from 'lodash'; +import { useSystemStore } from '@/web/common/system/useSystemStore'; type Props = FlowNodeItemType & { children?: React.ReactNode | React.ReactNode[] | string; @@ -69,6 +72,9 @@ const NodeCard = (props: Props) => { const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const onResetNode = useContextSelector(WorkflowContext, (v) => v.onResetNode); + const [hasNewVersion, setHasNewVersion] = useState(false); + const { setLoading } = useSystemStore(); + // custom title edit const { onOpenModal: onOpenCustomTitleModal, EditModal: EditTitleModal } = useEditTitle({ title: t('common.Custom Title'), @@ -81,13 +87,50 @@ const NodeCard = (props: Props) => { ); const node = nodeList.find((node) => node.nodeId === nodeId); + const { openConfirm: onOpenConfirmSync, ConfirmModal: ConfirmSyncModal } = useConfirm({ + content: appT('module.Confirm Sync') + }); + + useEffect(() => { + const fetchPluginModule = async () => { + if (node?.flowNodeType === FlowNodeTypeEnum.pluginModule) { + if (!node?.pluginId) return; + const template = await getPreviewPluginModule(node.pluginId); + setHasNewVersion(!!template.nodeVersion && node.nodeVersion !== template.nodeVersion); + } else { + const template = moduleTemplatesFlat.find( + (item) => item.flowNodeType === node?.flowNodeType + ); + setHasNewVersion(node?.version !== template?.version); + } + }; + + fetchPluginModule(); + }, [node]); + const template = moduleTemplatesFlat.find((item) => item.flowNodeType === node?.flowNodeType); - const hasNewVersion = useMemo(() => { - return ( - template?.flowNodeType !== FlowNodeTypeEnum.pluginModule && - node?.version !== template?.version - ); - }, [node?.version, template?.flowNodeType, template?.version]); + + const onClickSyncVersion = useCallback(async () => { + try { + setLoading(true); + if (!node || !template) return; + if (node?.flowNodeType === 'pluginModule') { + if (!node.pluginId) return; + onResetNode({ + id: nodeId, + node: await getPreviewPluginModule(node.pluginId) + }); + } else { + onResetNode({ + id: nodeId, + node: updateFlowNodeVersion(node, template) + }); + } + } catch (error) { + console.error('Error fetching plugin module:', error); + } + setLoading(false); + }, [node, nodeId, onResetNode, setLoading, template]); /* Node header */ const Header = useMemo(() => { @@ -149,13 +192,7 @@ const NodeCard = (props: Props) => { fontWeight={'medium'} cursor={'pointer'} _hover={{ bg: 'yellow.100' }} - onClick={() => { - if (!node || !template) return; - onResetNode({ - id: nodeId, - node: updateFlowNodeVersion(node, template) - }); - }} + onClick={onOpenConfirmSync(onClickSyncVersion)} > {appT('app.modules.has new version')} @@ -171,6 +208,7 @@ const NodeCard = (props: Props) => { /> + ); }, [ @@ -181,16 +219,16 @@ const NodeCard = (props: Props) => { name, menuForbid, hasNewVersion, + appT, + onOpenConfirmSync, + onClickSyncVersion, pluginId, flowNodeType, intro, + ConfirmSyncModal, onOpenCustomTitleModal, onChangeNode, - toast, - appT, - node, - template, - onResetNode + toast ]); return ( @@ -249,21 +287,14 @@ const MenuRender = React.memo(function MenuRender({ menuForbid?: Props['menuForbid']; }) { const { t } = useTranslation(); - const { toast } = useToast(); - const { setLoading } = useSystemStore(); const { openDebugNode, DebugInputModal } = useDebug(); - const { openConfirm: onOpenConfirmSync, ConfirmModal: ConfirmSyncModal } = useConfirm({ - content: t('module.Confirm Sync Plugin') - }); - const { openConfirm: onOpenConfirmDeleteNode, ConfirmModal: ConfirmDeleteModal } = useConfirm({ content: t('core.module.Confirm Delete Node'), type: 'delete' }); const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes); - const onResetNode = useContextSelector(WorkflowContext, (v) => v.onResetNode); const setEdges = useContextSelector(WorkflowContext, (v) => v.setEdges); const onCopyNode = useCallback( @@ -310,22 +341,6 @@ const MenuRender = React.memo(function MenuRender({ }, [setEdges, setNodes] ); - const onClickSyncVersion = useCallback(async () => { - if (!pluginId) return; - try { - setLoading(true); - onResetNode({ - id: nodeId, - node: await getPreviewPluginModule(pluginId) - }); - } catch (e) { - return toast({ - status: 'error', - title: getErrText(e, t('plugin.Get Plugin Module Detail Failed')) - }); - } - setLoading(false); - }, [nodeId, onResetNode, pluginId, setLoading, t, toast]); const Render = useMemo(() => { const menuList = [ @@ -349,17 +364,6 @@ const MenuRender = React.memo(function MenuRender({ onClick: () => onCopyNode(nodeId) } ]), - ...(flowNodeType === FlowNodeTypeEnum.pluginModule - ? [ - { - icon: 'common/refreshLight', - label: t('plugin.Synchronous version'), - variant: 'whiteBase', - onClick: onOpenConfirmSync(onClickSyncVersion) - } - ] - : []), - ...(menuForbid?.delete ? [] : [ @@ -401,7 +405,6 @@ const MenuRender = React.memo(function MenuRender({ ))} - @@ -411,11 +414,7 @@ const MenuRender = React.memo(function MenuRender({ menuForbid?.copy, menuForbid?.delete, t, - flowNodeType, - onOpenConfirmSync, - onClickSyncVersion, onOpenConfirmDeleteNode, - ConfirmSyncModal, ConfirmDeleteModal, DebugInputModal, openDebugNode, diff --git a/projects/app/src/components/core/workflow/utils.ts b/projects/app/src/components/core/workflow/utils.ts index de7db1f17..19c8bef76 100644 --- a/projects/app/src/components/core/workflow/utils.ts +++ b/projects/app/src/components/core/workflow/utils.ts @@ -22,7 +22,8 @@ export const flowNode2StoreNodes = ({ version: item.data.version, inputs: item.data.inputs, outputs: item.data.outputs, - pluginId: item.data.pluginId + pluginId: item.data.pluginId, + nodeVersion: item.data.nodeVersion })); // get all handle diff --git a/projects/app/src/pages/api/core/plugin/update.ts b/projects/app/src/pages/api/core/plugin/update.ts index ec716055e..d85214ffa 100644 --- a/projects/app/src/pages/api/core/plugin/update.ts +++ b/projects/app/src/pages/api/core/plugin/update.ts @@ -7,6 +7,8 @@ import { MongoPlugin } from '@fastgpt/service/core/plugin/schema'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { ClientSession } from '@fastgpt/service/common/mongo'; import { httpApiSchema2Plugins } from '@fastgpt/global/core/plugin/httpPlugin/utils'; +import { isEqual } from 'lodash'; +import { nanoid } from 'nanoid'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { @@ -22,7 +24,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< per: 'owner' }); - const updateData = { + const originPlugin = await MongoPlugin.findById(id); + + let updateData = { name: props.name, intro: props.intro, avatar: props.avatar, @@ -32,9 +36,26 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< modules: modules }), ...(edges?.length && { edges }), - metadata: props.metadata + metadata: props.metadata, + nodeVersion: originPlugin?.nodeVersion }; + const isNodeVersionEqual = + isEqual( + originPlugin?.modules.map((module) => { + return { ...module, position: undefined }; + }), + updateData.modules?.map((module) => { + return { ...module, position: undefined }; + }) + ) && isEqual(originPlugin?.edges, updateData.edges); + + if (!isNodeVersionEqual) { + updateData = { + ...updateData, + nodeVersion: nanoid(6) + }; + } if (props.metadata?.apiSchemaStr) { await mongoSessionRun(async (session) => { // update children