Feat: Node latest version (#4905)

* node versions add keep the latest option (#4899)

* node versions add keep the latest option

* i18n

* perf: version code

* fix: ts

* hide system version

* hide system version

* hide system version

* fix: ts

* fix: ts

---------

Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
Archer
2025-05-28 10:46:32 +08:00
committed by GitHub
parent 331b851a78
commit b4ecfb0b79
50 changed files with 250 additions and 192 deletions

View File

@@ -24,7 +24,7 @@ export type GetChatRecordsProps = OutLinkChatAuthProps & {
appId: string;
chatId?: string;
loadCustomFeedbacks?: boolean;
type: `${GetChatTypeEnum}`;
type?: `${GetChatTypeEnum}`;
};
export type InitOutLinkChatProps = {

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useMemo, useRef } from 'react';
import { Box, Button, Flex, HStack, useDisclosure, type FlexProps } from '@chakra-ui/react';
import React, { useCallback, useMemo } from 'react';
import { Box, Button, Flex, 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';
@@ -15,7 +15,7 @@ import { ToolSourceHandle, ToolTargetHandle } from './Handle/ToolHandle';
import { useEditTextarea } from '@fastgpt/web/hooks/useEditTextarea';
import { ConnectionSourceHandle, ConnectionTargetHandle } from './Handle/ConnectionHandle';
import { useDebug } from '../../hooks/useDebug';
import { getPreviewPluginNode } from '@/web/core/app/api/plugin';
import { getPreviewPluginNode, getToolVersionList } from '@/web/core/app/api/plugin';
import { storeNode2FlowNode } from '@/web/core/workflow/utils';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { useContextSelector } from 'use-context-selector';
@@ -104,12 +104,9 @@ const NodeCard = (props: Props) => {
}, [nodeList, nodeId]);
const isAppNode = node && AppNodeFlowNodeTypeMap[node?.flowNodeType];
const showVersion = useMemo(() => {
if (!isAppNode || !node?.pluginId) return false;
if (!isAppNode || !node?.pluginId || node?.pluginData?.error) return false;
if ([FlowNodeTypeEnum.tool, FlowNodeTypeEnum.toolSet].includes(node.flowNodeType)) return false;
if (node.pluginId.split('-').length > 1) {
return false;
}
return true;
return typeof node.version === 'string';
}, [isAppNode, node]);
const { data: nodeTemplate } = useRequest2(
@@ -617,11 +614,10 @@ const NodeVersion = React.memo(function NodeVersion({ node }: { node: FlowNodeIt
const { isOpen, onOpen, onClose } = useDisclosure();
// Load version list
const { ScrollData, data: versionList } = useScrollPagination(getAppVersionList, {
const { ScrollData, data: versionList } = useScrollPagination(getToolVersionList, {
pageSize: 20,
params: {
appId: node.pluginId,
isPublish: true
toolId: node.pluginId
},
refreshDeps: [node.pluginId, isOpen],
disalbed: !isOpen,
@@ -653,18 +649,23 @@ const NodeVersion = React.memo(function NodeVersion({ node }: { node: FlowNodeIt
}
);
const renderList = useCreation(
() =>
versionList.map((item) => ({
const renderVersionList = useCreation(
() => [
{
label: t('app:keep_the_latest'),
value: ''
},
...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?.version === '' ? t('app:keep_the_latest') : node?.versionLabel}
{!node.isLatestVersion && (
<MyTag type="fill" colorSchema={'adora'} fontSize={'mini'} borderRadius={'lg'}>
{t('app:not_the_newest')}
@@ -672,7 +673,7 @@ const NodeVersion = React.memo(function NodeVersion({ node }: { node: FlowNodeIt
)}
</Flex>
);
}, [node.isLatestVersion, node?.versionLabel, t]);
}, [node.isLatestVersion, node?.version, node?.versionLabel, t]);
return (
<MySelect
@@ -685,7 +686,7 @@ const NodeVersion = React.memo(function NodeVersion({ node }: { node: FlowNodeIt
placeholder={node?.versionLabel}
variant={'whitePrimaryOutline'}
size={'sm'}
list={renderList}
list={renderVersionList}
ScrollData={(props) => (
<ScrollData minH={'100px'} maxH={'40vh'}>
{props.children}

View File

@@ -3,7 +3,6 @@ import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { checkNode } from '@/service/core/app/utils';
import { rewriteAppWorkflowToDetail } from '@fastgpt/service/core/app/utils';
/* 获取应用详情 */
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -23,6 +22,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
await rewriteAppWorkflowToDetail({
nodes: app.modules,
teamId,
ownerTmbId: app.tmbId,
isRoot
});
@@ -34,12 +34,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
};
}
return {
...app,
modules: await Promise.all(
app.modules.map((node) => checkNode({ node, ownerTmbId: app.tmbId }))
)
};
return app;
}
export default NextAPI(handler);

View File

@@ -4,7 +4,7 @@
import type { NextApiResponse } from 'next';
import {
getChildAppPreviewNode,
splitCombinePluginId
splitCombineToolId
} from '@fastgpt/service/core/app/plugin/controller';
import { type FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node.d';
import { NextAPI } from '@/service/middleware/entry';
@@ -21,7 +21,7 @@ async function handler(
): Promise<FlowNodeTemplateType> {
const { appId, versionId } = req.query;
const { source } = await splitCombinePluginId(appId);
const { source } = splitCombineToolId(appId);
if (source === PluginSourceEnum.personal) {
await authApp({ req, authToken: true, appId, per: ReadPermissionVal });

View File

@@ -0,0 +1,86 @@
import type { NextApiResponse } from 'next';
import { NextAPI } from '@/service/middleware/entry';
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
import { type PaginationProps, type PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { type ApiRequestProps } from '@fastgpt/service/type/next';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
import { splitCombineToolId } from '@fastgpt/service/core/app/plugin/controller';
import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
import { getSystemPluginTemplates } from '@fastgpt/plugins/register';
import { PluginErrEnum } from '@fastgpt/global/common/error/code/plugin';
import { Types } from '@fastgpt/service/common/mongo';
export type getToolVersionListProps = PaginationProps<{
toolId?: string;
}>;
export type getToolVersionResponse = PaginationResponse<{
_id: string;
versionName: string;
}>;
async function handler(
req: ApiRequestProps<getToolVersionListProps>,
_res: NextApiResponse<any>
): Promise<getToolVersionResponse> {
const { toolId } = req.body;
const { offset, pageSize } = parsePaginationRequest(req);
if (!toolId) {
return {
total: 0,
list: []
};
}
const { source, pluginId: formatToolId } = splitCombineToolId(toolId);
// Auth
const appId = await (async () => {
if (source === PluginSourceEnum.personal) {
const { app } = await authApp({
appId: formatToolId,
req,
per: ReadPermissionVal,
authToken: true
});
return app._id;
} else {
const item = getSystemPluginTemplates().find((plugin) => plugin.id === formatToolId);
if (!item) return Promise.reject(PluginErrEnum.unAuth);
return item.associatedPluginId;
}
})();
if (!appId || !Types.ObjectId.isValid(appId)) {
return {
total: 0,
list: []
};
}
const match = {
appId,
isPublish: true
};
const [result, total] = await Promise.all([
await MongoAppVersion.find(match, 'versionName')
.sort({
time: -1
})
.skip(offset)
.limit(pageSize)
.lean(),
MongoAppVersion.countDocuments(match)
]);
return {
total,
list: result
};
}
export default NextAPI(handler);

View File

@@ -5,7 +5,6 @@ import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { type AppVersionSchemaType } from '@fastgpt/global/core/app/version';
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
import { checkNode } from '@/service/core/app/utils';
import { rewriteAppWorkflowToDetail } from '@fastgpt/service/core/app/utils';
type Props = {
@@ -34,14 +33,12 @@ async function handler(
await rewriteAppWorkflowToDetail({
nodes: result.nodes,
teamId,
ownerTmbId: app.tmbId,
isRoot
});
return {
...result,
nodes: await Promise.all(
result.nodes.map((n) => checkNode({ node: n, ownerTmbId: app.tmbId }))
),
versionName: result?.versionName || formatTime2YMDHM(result?.time)
};
}

View File

@@ -31,13 +31,16 @@ async function handler(
per: WritePermissionVal
});
const version = await getAppLatestVersion(req.query.appId, app);
await rewriteAppWorkflowToDetail({
nodes: app.modules,
nodes: version.nodes,
teamId,
isRoot
isRoot,
ownerTmbId: app.tmbId
});
return getAppLatestVersion(req.query.appId, app);
return version;
}
export default NextAPI(handler);

View File

@@ -24,7 +24,7 @@ import { saveChat } from '@fastgpt/service/core/chat/saveChat';
import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller';
import {
getChildAppPreviewNode,
splitCombinePluginId
splitCombineToolId
} from '@fastgpt/service/core/app/plugin/controller';
import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
import { authAppByTmbId } from '@fastgpt/service/support/permission/app/auth';
@@ -137,46 +137,3 @@ export const getScheduleTriggerApp = async () => {
})
);
};
export const checkNode = async ({
node,
ownerTmbId
}: {
node: StoreNodeItemType;
ownerTmbId: string;
}): Promise<StoreNodeItemType> => {
const pluginId = node.pluginId;
if (!pluginId) return node;
try {
const { source } = await splitCombinePluginId(pluginId);
if (source === PluginSourceEnum.personal) {
await authAppByTmbId({
tmbId: ownerTmbId,
appId: pluginId,
per: ReadPermissionVal
});
}
const preview = await getChildAppPreviewNode({ appId: pluginId });
return {
...node,
pluginData: {
version: preview.version,
diagram: preview.diagram,
userGuide: preview.userGuide,
courseUrl: preview.courseUrl,
name: preview.name,
avatar: preview.avatar
}
};
} catch (error: any) {
return {
...node,
pluginData: {
error: getErrText(error)
}
};
}
};

View File

@@ -24,6 +24,10 @@ import { type McpToolConfigType } from '@fastgpt/global/core/app/type';
import type { updateMCPToolsBody } from '@/pages/api/core/app/mcpTools/update';
import type { RunMCPToolBody } from '@/pages/api/support/mcp/client/runTool';
import type { getMCPToolsBody } from '@/pages/api/support/mcp/client/getTools';
import type {
getToolVersionListProps,
getToolVersionResponse
} from '@/pages/api/core/app/plugin/getVersionList';
/* ============ team plugin ============== */
export const getTeamPlugTemplates = (data?: ListAppBody) =>
@@ -71,6 +75,9 @@ export const getSystemPluginPaths = (data: GetPathProps) => {
export const getPreviewPluginNode = (data: GetPreviewNodeQuery) =>
GET<FlowNodeTemplateType>('/core/app/plugin/getPreviewNode', data);
export const getToolVersionList = (data: getToolVersionListProps) =>
POST<getToolVersionResponse>('/core/app/plugin/getVersionList', data);
/* ============ mcp tools ============== */
export const postCreateMCPTools = (data: createMCPToolsBody) =>
POST('/core/app/mcpTools/create', data);

View File

@@ -51,7 +51,7 @@ const ChatRecordContextProvider = ({
params
}: {
children: ReactNode;
params: Record<string, any>;
params: Omit<getPaginationRecordsBody, 'offset' | 'pageSize'>;
}) => {
const ChatBoxRef = useContextSelector(ChatItemContext, (v) => v.ChatBoxRef);
const [isChatRecordsLoaded, setIsChatRecordsLoaded] = useState(false);