Test version (#4792)

* plugin node version select (#4760)

* plugin node version select

* type

* fix

* fix

* perf: version list

* fix node version (#4787)

* change my select

* fix-ui

* fix test

* add test

* fix

* remove invalid version field

* filter deprecated field

* fix: claude tool call

* fix: test

---------

Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
Archer
2025-05-12 22:27:01 +08:00
committed by GitHub
parent 3cc6b8a17a
commit 0ef3d40296
69 changed files with 1024 additions and 599 deletions

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import {
getAppVersionDetail,
getWorkflowVersionList,
getAppVersionList,
updateAppVersion
} from '@/web/core/app/api/version';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
@@ -186,7 +186,7 @@ const TeamCloud = ({
ScrollData,
data: scrollDataList,
setData
} = useScrollPagination(getWorkflowVersionList, {
} = useScrollPagination(getAppVersionList, {
pageSize: 30,
params: {
appId: appDetail._id

View File

@@ -24,7 +24,10 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import {
AppNodeFlowNodeTypeMap,
FlowNodeTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import {
getPreviewPluginNode,
getSystemPlugTemplates,
@@ -475,11 +478,7 @@ const RenderList = React.memo(function RenderList({
const templateNode = await (async () => {
try {
// get plugin preview module
if (
template.flowNodeType === FlowNodeTypeEnum.pluginModule ||
template.flowNodeType === FlowNodeTypeEnum.appModule ||
template.flowNodeType === FlowNodeTypeEnum.toolSet
) {
if (AppNodeFlowNodeTypeMap[template.flowNodeType]) {
setLoading(true);
const res = await getPreviewPluginNode({ appId: template.id });
@@ -533,21 +532,25 @@ const RenderList = React.memo(function RenderList({
pluginId: templateNode.pluginId
}),
intro: t(templateNode.intro as any),
inputs: templateNode.inputs.map((input) => ({
...input,
value: defaultValueMap[input.key] ?? input.value,
valueDesc: t(input.valueDesc as any),
label: t(input.label as any),
description: t(input.description as any),
debugLabel: t(input.debugLabel as any),
toolDescription: t(input.toolDescription as any)
})),
outputs: templateNode.outputs.map((output) => ({
...output,
valueDesc: t(output.valueDesc as any),
label: t(output.label as any),
description: t(output.description as any)
}))
inputs: templateNode.inputs
.filter((input) => input.deprecated !== true)
.map((input) => ({
...input,
value: defaultValueMap[input.key] ?? input.value,
valueDesc: t(input.valueDesc as any),
label: t(input.label as any),
description: t(input.description as any),
debugLabel: t(input.debugLabel as any),
toolDescription: t(input.toolDescription as any)
})),
outputs: templateNode.outputs
.filter((output) => output.deprecated !== true)
.map((output) => ({
...output,
valueDesc: t(output.valueDesc as any),
label: t(output.label as any),
description: t(output.description as any)
}))
},
position: { x: mouseX, y: mouseY },
selected: true,

View File

@@ -1,13 +1,15 @@
import React, { useCallback, useMemo } from 'react';
import { Box, Button, Flex, type FlexProps } from '@chakra-ui/react';
import React, { useCallback, useMemo, useRef } from 'react';
import { Box, Button, Flex, HStack, useDisclosure, type FlexProps } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar';
import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.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 { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import {
AppNodeFlowNodeTypeMap,
FlowNodeTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { LOGO_ICON } from '@fastgpt/global/common/system/constants';
import { ToolSourceHandle, ToolTargetHandle } from './Handle/ToolHandle';
import { useEditTextarea } from '@fastgpt/web/hooks/useEditTextarea';
@@ -28,6 +30,11 @@ import MyImage from '@fastgpt/web/components/common/Image/MyImage';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import UseGuideModal from '@/components/common/Modal/UseGuideModal';
import NodeDebugResponse from './RenderDebug/NodeDebugResponse';
import { getAppVersionList } from '@/web/core/app/api/version';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { useCreation } from 'ahooks';
type Props = FlowNodeItemType & {
children?: React.ReactNode | React.ReactNode[] | string;
@@ -61,7 +68,6 @@ const NodeCard = (props: Props) => {
w = 'full',
h = 'full',
nodeId,
flowNodeType,
selected,
menuForbid,
isTool = false,
@@ -73,7 +79,6 @@ const NodeCard = (props: Props) => {
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const onResetNode = useContextSelector(WorkflowContext, (v) => v.onResetNode);
const setHoverNodeId = useContextSelector(WorkflowEventContext, (v) => v.setHoverNodeId);
// custom title edit
@@ -96,6 +101,7 @@ const NodeCard = (props: Props) => {
return { node, parentNode };
}, [nodeList, nodeId]);
const isAppNode = node && AppNodeFlowNodeTypeMap[node?.flowNodeType];
const { data: nodeTemplate } = useRequest2(
async () => {
@@ -103,12 +109,7 @@ const NodeCard = (props: Props) => {
return undefined;
}
if (
node?.flowNodeType === FlowNodeTypeEnum.pluginModule ||
node?.flowNodeType === FlowNodeTypeEnum.appModule ||
node?.flowNodeType === FlowNodeTypeEnum.tool ||
node?.flowNodeType === FlowNodeTypeEnum.toolSet
) {
if (isAppNode) {
return { ...node, ...node.pluginData };
} else {
const template = moduleTemplatesFlat.find(
@@ -132,51 +133,6 @@ const NodeCard = (props: Props) => {
}
);
const {
openConfirm: onOpenConfirmSync,
onClose: onCloseConfirmSync,
ConfirmModal: ConfirmSyncModal
} = useConfirm({
content: t('workflow:Confirm_sync_node')
});
const hasNewVersion = nodeTemplate && nodeTemplate.version !== node?.version;
const { runAsync: onClickSyncVersion } = useRequest2(
async () => {
if (!node) return;
if (node.pluginId) {
const template = await getPreviewPluginNode({ appId: node.pluginId });
if (!!template) {
onResetNode({
id: nodeId,
node: template
});
}
} else {
const template = moduleTemplatesFlat.find(
(item) => item.flowNodeType === node.flowNodeType
);
if (!template) {
return toast({
title: t('app:app.modules.not_found_tips'),
status: 'warning'
});
}
onResetNode({
id: nodeId,
node: template
});
}
},
{
refreshDeps: [node, nodeId, onResetNode],
onFinally() {}
}
);
/* Node header */
const Header = useMemo(() => {
const showHeader = node?.flowNodeType !== FlowNodeTypeEnum.comment;
@@ -255,28 +211,9 @@ const NodeCard = (props: Props) => {
>
<MyIcon name={'edit'} w={'14px'} />
</Button>
<Box flex={1} />
{hasNewVersion && (
<MyTooltip label={t('app:app.modules.click to update')}>
<Button
bg={'yellow.50'}
color={'yellow.600'}
variant={'ghost'}
h={8}
px={2}
rounded={'6px'}
fontSize={'xs'}
fontWeight={'medium'}
cursor={'pointer'}
_hover={{ bg: 'yellow.100' }}
onClick={onOpenConfirmSync(onClickSyncVersion)}
>
<Box>{t('app:app.modules.has new version')}</Box>
<MyIcon name={'help'} w={'14px'} ml={1} />
</Button>
</MyTooltip>
)}
{!!nodeTemplate?.diagram && !hasNewVersion && (
<Box flex={1} mr={1} />
{isAppNode && <NodeVersion node={node} />}
{!!nodeTemplate?.diagram && (
<MyTooltip
label={
<MyImage
@@ -295,7 +232,7 @@ const NodeCard = (props: Props) => {
{!!nodeTemplate?.diagram && node?.courseUrl && (
<Box bg={'myGray.300'} w={'1px'} h={'12px'} ml={1} mr={0.5} />
)}
{!!(node?.courseUrl || nodeTemplate?.userGuide) && !hasNewVersion && (
{!!(node?.courseUrl || nodeTemplate?.userGuide) && (
<UseGuideModal
title={nodeTemplate?.name}
iconSrc={nodeTemplate?.avatar}
@@ -310,7 +247,7 @@ const NodeCard = (props: Props) => {
</UseGuideModal>
)}
{!!node?.pluginData?.error && (
<MyTooltip label={t('app:app.modules.not_found_tips')}>
<MyTooltip label={node?.pluginData?.error || t('app:app.modules.not_found_tips')}>
<Flex
bg={'red.50'}
alignItems={'center'}
@@ -321,7 +258,9 @@ const NodeCard = (props: Props) => {
fontWeight={'medium'}
>
<MyIcon name={'common/errorFill'} w={'14px'} mr={1} />
<Box color={'red.600'}>{t('app:app.modules.not_found')}</Box>
<Box color={'red.600'}>
{node?.pluginData?.error || t('app:app.modules.not_found')}
</Box>
</Flex>
</MyTooltip>
)}
@@ -333,18 +272,14 @@ const NodeCard = (props: Props) => {
</Box>
);
}, [
node?.flowNodeType,
node?.courseUrl,
node?.pluginData?.error,
node,
showToolHandle,
nodeId,
isFolded,
avatar,
t,
name,
hasNewVersion,
onOpenConfirmSync,
onClickSyncVersion,
isAppNode,
nodeTemplate?.diagram,
nodeTemplate?.userGuide,
nodeTemplate?.name,
@@ -419,7 +354,6 @@ const NodeCard = (props: Props) => {
{RenderHandle}
{RenderToolHandle}
<ConfirmSyncModal />
<EditTitleModal maxLength={100} />
</Flex>
);
@@ -663,3 +597,90 @@ const NodeIntro = React.memo(function NodeIntro({
return Render;
});
const NodeVersion = React.memo(function NodeVersion({ node }: { node: FlowNodeItemType }) {
const { t } = useTranslation();
const onResetNode = useContextSelector(WorkflowContext, (v) => v.onResetNode);
const { isOpen, onOpen, onClose } = useDisclosure();
// Load version list
const { ScrollData, data: versionList } = useScrollPagination(getAppVersionList, {
pageSize: 20,
params: {
appId: node.pluginId,
isPublish: true
},
refreshDeps: [node.pluginId, isOpen],
disalbed: !isOpen,
manual: false
});
const { runAsync: onUpdateVersion, loading: isUpdating } = useRequest2(
async (versionId: string) => {
if (!node) return;
if (node.pluginId) {
const template = await getPreviewPluginNode({ appId: node.pluginId, versionId });
if (!!template) {
onResetNode({
id: node.nodeId,
node: {
...template,
name: node.name,
intro: node.intro,
avatar: node.avatar
}
});
}
}
},
{
refreshDeps: [node, onResetNode]
}
);
const renderList = useCreation(
() =>
versionList.map((item) => ({
label: item.versionName,
value: item._id
})),
[node.isLatestVersion, node.version, t, versionList]
);
const valueLabel = useMemo(() => {
return (
<Flex alignItems={'center'} gap={0.5}>
{node?.versionLabel}
{!node.isLatestVersion && (
<MyTag type="fill" colorSchema={'adora'} fontSize={'mini'} borderRadius={'lg'}>
{t('app:not_the_newest')}
</MyTag>
)}
</Flex>
);
}, [node.isLatestVersion, node?.versionLabel, t]);
return (
<MySelect
className="nowheel"
value={node.version}
onChange={onUpdateVersion}
isLoading={isUpdating}
customOnOpen={onOpen}
customOnClose={onClose}
placeholder={node?.versionLabel}
variant={'whitePrimaryOutline'}
size={'sm'}
list={renderList}
ScrollData={(props) => (
<ScrollData minH={'100px'} maxH={'40vh'}>
{props.children}
</ScrollData>
)}
valueLabel={valueLabel}
/>
);
});

View File

@@ -10,6 +10,8 @@ import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pageComponents/app/detail/WorkflowComponents/context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
type Props = {
nodeId: string;
@@ -68,8 +70,38 @@ const InputLabel = ({ nodeId, input, RightComponent }: Props) => {
</Box>
)}
{input.deprecated && (
<>
<Box flex={'1'} />
<MyTooltip label={t('app:Click_to_delete_this_field')}>
<Flex
px={1.5}
py={1}
bg={'adora.50'}
rounded={'6px'}
fontSize={'14px'}
cursor="pointer"
alignItems={'center'}
_hover={{
bg: 'adora.100'
}}
onClick={() => {
onChangeNode({
nodeId,
type: 'delInput',
key: input.key
});
}}
>
<MyIcon name={'common/info'} color={'adora.600'} w={4} mr={1} />
<Box color={'adora.600'}>{t('app:Filed_is_deprecated')}</Box>
</Flex>
</MyTooltip>
</>
)}
{/* Right Component */}
{RightComponent && (
{!input.deprecated && RightComponent && (
<>
<Box flex={'1'} />
{RightComponent}

View File

@@ -8,11 +8,17 @@ import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { Position } from 'reactflow';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import ValueTypeLabel from '../ValueTypeLabel';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../../context';
const OutputLabel = ({ nodeId, output }: { nodeId: string; output: FlowNodeOutputItemType }) => {
const { t } = useTranslation();
const { label = '', description, valueType, valueDesc } = output;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
return (
<Box position={'relative'}>
<Flex
@@ -36,6 +42,36 @@ const OutputLabel = ({ nodeId, output }: { nodeId: string; output: FlowNodeOutpu
</Box>
{description && <QuestionTip ml={1} label={t(description as any)} />}
<ValueTypeLabel valueType={valueType} valueDesc={valueDesc} />
{output.deprecated && (
<>
<Box flex={'1'} />
<MyTooltip label={t('app:Click_to_delete_this_field')}>
<Flex
px={1.5}
py={1}
bg={'adora.50'}
rounded={'6px'}
fontSize={'14px'}
cursor="pointer"
alignItems={'center'}
_hover={{
bg: 'adora.100'
}}
onClick={() => {
onChangeNode({
nodeId,
type: 'delOutput',
key: output.key
});
}}
>
<MyIcon name={'common/info'} color={'adora.600'} w={4} mr={1} />
<Box color={'adora.600'}>{t('app:Filed_is_deprecated')}</Box>
</Flex>
</MyTooltip>
</>
)}
</Flex>
{output.type === FlowNodeOutputTypeEnum.source && (
<SourceHandle