Commit 4fbabbd8 by light5980 Committed by GitHub

debug: import compatibility issue (#7099)

* debug: import compatibility issue

* fix: improve app import compatibility

---------

Co-authored-by: archer <545436317@qq.com>
parent e6de4950
--- ---
name: pr-review name: pr-review
description: 当用户传入一个 review 的 pr 链接时候,触发该 skill,对 pr 进行代码审查 description: 仅当用户明确手动指定使用 pr-review skill 时触发;不要因为用户传入 PR 链接、要求 review 或要求代码审查而自动触发
--- ---
# PR Review 代码审查技能 # PR Review 代码审查技能
> 按阶段对 Pull Request 进行系统性审查,先验证需求理解与逻辑正确性,再并行进行多维度质量检测,最后提交审查报告。 > 按阶段对 Pull Request 进行系统性审查,先验证需求理解与逻辑正确性,再并行进行多维度质量检测,最后提交审查报告。
**触发约束**:本 skill 必须由用户明确点名 `pr-review``$pr-review` 或“使用 pr-review skill”才可使用。普通 PR 链接、`review 下这个 PR``帮我代码审查` 等请求不应自动触发本 skill。
--- ---
## 步骤 0:拉取代码 ## 步骤 0:拉取代码
......
...@@ -209,9 +209,16 @@ const MyMenu = ({ ...@@ -209,9 +209,16 @@ const MyMenu = ({
const formatTrigger = !isPc ? 'click' : trigger; const formatTrigger = !isPc ? 'click' : trigger;
const isIgnoreOutsideClickTarget = (event: Event) => {
return event.composedPath().some((target) => {
return target instanceof HTMLElement && target.dataset.myMenuIgnoreOutsideClick !== undefined;
});
};
useOutsideClick({ useOutsideClick({
ref: ref, ref: ref,
handler: () => { handler: (event) => {
if (isIgnoreOutsideClickTarget(event)) return;
setIsOpen(false); setIsOpen(false);
} }
}); });
...@@ -245,7 +252,7 @@ const MyMenu = ({ ...@@ -245,7 +252,7 @@ const MyMenu = ({
if (formatTrigger === 'hover') { if (formatTrigger === 'hover') {
closeTimer.current = setTimeout(() => { closeTimer.current = setTimeout(() => {
setIsOpen(false); setIsOpen(false);
}, 100); }, 250);
} }
}} }}
> >
...@@ -294,6 +301,7 @@ const MyMenu = ({ ...@@ -294,6 +301,7 @@ const MyMenu = ({
const menuItem = ( const menuItem = (
<MenuItem <MenuItem
key={index} key={index}
w={'100%'}
borderRadius={'sm'} borderRadius={'sm'}
isDisabled={child.disabled} isDisabled={child.disabled}
onClick={(e) => { onClick={(e) => {
......
...@@ -22,6 +22,8 @@ interface Props extends PopoverContentProps { ...@@ -22,6 +22,8 @@ interface Props extends PopoverContentProps {
onCloseFunc?: () => void; onCloseFunc?: () => void;
onOpenFunc?: () => void; onOpenFunc?: () => void;
closeOnBlur?: boolean; closeOnBlur?: boolean;
usePortal?: boolean;
flip?: boolean;
} }
const MyPopover = ({ const MyPopover = ({
...@@ -34,6 +36,8 @@ const MyPopover = ({ ...@@ -34,6 +36,8 @@ const MyPopover = ({
onOpenFunc, onOpenFunc,
onCloseFunc, onCloseFunc,
closeOnBlur = false, closeOnBlur = false,
usePortal = true,
flip = true,
onBackdropClick, onBackdropClick,
...props ...props
}: Props) => { }: Props) => {
...@@ -41,6 +45,13 @@ const MyPopover = ({ ...@@ -41,6 +45,13 @@ const MyPopover = ({
const { onOpen, onClose, isOpen } = useDisclosure(); const { onOpen, onClose, isOpen } = useDisclosure();
const popoverContent = (
<PopoverContent zIndex={1001} {...props}>
{hasArrow && <PopoverArrow />}
{children({ onClose })}
</PopoverContent>
);
return ( return (
<Popover <Popover
isOpen={isOpen} isOpen={isOpen}
...@@ -55,6 +66,7 @@ const MyPopover = ({ ...@@ -55,6 +66,7 @@ const MyPopover = ({
}} }}
placement={placement} placement={placement}
offset={offset} offset={offset}
flip={flip}
closeOnBlur={closeOnBlur} closeOnBlur={closeOnBlur}
trigger={trigger} trigger={trigger}
openDelay={100} openDelay={100}
...@@ -69,12 +81,7 @@ const MyPopover = ({ ...@@ -69,12 +81,7 @@ const MyPopover = ({
<Box position="fixed" zIndex={1000} inset={0} onClick={() => onBackdropClick()} /> <Box position="fixed" zIndex={1000} inset={0} onClick={() => onBackdropClick()} />
</Portal> </Portal>
)} )}
<Portal> {usePortal ? <Portal>{popoverContent}</Portal> : popoverContent}
<PopoverContent zIndex={1001} {...props}>
{hasArrow && <PopoverArrow />}
{children({ onClose })}
</PopoverContent>
</Portal>
</Popover> </Popover>
); );
}; };
......
...@@ -131,20 +131,38 @@ const ExportConfigPopover = ({ ...@@ -131,20 +131,38 @@ const ExportConfigPopover = ({
return ( return (
<MyPopover <MyPopover
placement={'right-start'} placement={'right-start'}
offset={[0, 20]} offset={[0, 0]}
hasArrow hasArrow={false}
trigger={'hover'} trigger={'hover'}
flip={false}
zIndex={2000}
data-my-menu-ignore-outside-click
w={'8.8rem'} w={'8.8rem'}
Trigger={ Trigger={
<MyBox display={'flex'} cursor={'pointer'} onClick={(e) => e.stopPropagation()}> <MyBox
<MyIcon name={'export'} w={'16px'} mr={2} /> display={'flex'}
<Box fontSize={'sm'}>{t('app:export_configs')}</Box> alignItems={'center'}
w={'100%'}
cursor={'pointer'}
onClick={(e) => e.stopPropagation()}
>
<MyIcon name={'export'} w={'16px'} mr={2} flexShrink={0} />
<Box flex={1} minW={0} fontSize={'sm'}>
{t('app:export_configs')}
</Box>
</MyBox> </MyBox>
} }
> >
{() => ( {() => (
<Box p={1} onClick={(e) => e.stopPropagation()}> <Box
p={1}
w={'100%'}
onPointerDown={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
<Flex <Flex
w={'100%'}
py={'0.38rem'} py={'0.38rem'}
px={1} px={1}
color={'myGray.600'} color={'myGray.600'}
...@@ -156,10 +174,13 @@ const ExportConfigPopover = ({ ...@@ -156,10 +174,13 @@ const ExportConfigPopover = ({
borderRadius={'xs'} borderRadius={'xs'}
onClick={() => onExportWorkflow('copy')} onClick={() => onExportWorkflow('copy')}
> >
<MyIcon name={'copy'} w={'1rem'} mr={2} /> <MyIcon name={'copy'} w={'1rem'} mr={2} flexShrink={0} />
<Box fontSize={'mini'}>{t('common:copy_to_clipboard')}</Box> <Box flex={1} minW={0} fontSize={'mini'}>
{t('common:copy_to_clipboard')}
</Box>
</Flex> </Flex>
<Flex <Flex
w={'100%'}
py={'0.38rem'} py={'0.38rem'}
px={1} px={1}
color={'myGray.600'} color={'myGray.600'}
...@@ -171,13 +192,16 @@ const ExportConfigPopover = ({ ...@@ -171,13 +192,16 @@ const ExportConfigPopover = ({
borderRadius={'xs'} borderRadius={'xs'}
onClick={() => onExportWorkflow('json')} onClick={() => onExportWorkflow('json')}
> >
<MyIcon name={'configmap'} w={'1rem'} mr={2} /> <MyIcon name={'configmap'} w={'1rem'} mr={2} flexShrink={0} />
<Box fontSize={'mini'}>{t('common:export_to_json')}</Box> <Box flex={1} minW={0} fontSize={'mini'}>
{t('common:export_to_json')}
</Box>
</Flex> </Flex>
<Divider /> <Divider my={1} />
<Flex <Flex
w={'100%'}
py={'0.38rem'} py={'0.38rem'}
px={1} px={1}
alignItems={'center'} alignItems={'center'}
......
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Button, ModalBody, ModalFooter } from '@chakra-ui/react'; import { Button } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/v2/common/MyModal';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
...@@ -10,6 +10,8 @@ import { removeUnauthModels } from '@fastgpt/global/core/workflow/utils'; ...@@ -10,6 +10,8 @@ import { removeUnauthModels } from '@fastgpt/global/core/workflow/utils';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { WorkflowUtilsContext } from '../context/workflowUtilsContext'; import { WorkflowUtilsContext } from '../context/workflowUtilsContext';
import { parseWorkflowImportConfig } from '@/pageComponents/dashboard/agent/utils/appTemplateParse'; import { parseWorkflowImportConfig } from '@/pageComponents/dashboard/agent/utils/appTemplateParse';
import { AppContext } from '../../context';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
const ImportAppConfigEditor = dynamic(() => import('@/pageComponents/app/ImportAppConfigEditor'), { const ImportAppConfigEditor = dynamic(() => import('@/pageComponents/app/ImportAppConfigEditor'), {
ssr: false ssr: false
...@@ -23,6 +25,7 @@ const ImportSettings = ({ onClose }: Props) => { ...@@ -23,6 +25,7 @@ const ImportSettings = ({ onClose }: Props) => {
const { toast } = useToast(); const { toast } = useToast();
const initData = useContextSelector(WorkflowUtilsContext, (v) => v.initData); const initData = useContextSelector(WorkflowUtilsContext, (v) => v.initData);
const appType = useContextSelector(AppContext, (v) => v.appDetail.type);
const { t } = useTranslation(); const { t } = useTranslation();
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const { getMyModelList } = useSystemStore(); const { getMyModelList } = useSystemStore();
...@@ -35,18 +38,13 @@ const ImportSettings = ({ onClose }: Props) => { ...@@ -35,18 +38,13 @@ const ImportSettings = ({ onClose }: Props) => {
<MyModal <MyModal
isOpen isOpen
onClose={onClose} onClose={onClose}
iconSrc="common/importLight"
iconColor="primary.600"
title={t('app:import_configs')} title={t('app:import_configs')}
size={'md'} size={'md'}
> footer={
<ModalBody>
<ImportAppConfigEditor value={value} onChange={setValue} rows={16} />
</ModalBody>
<ModalFooter justifyItems={'flex-end'}>
<Button <Button
px={5} px={5}
py={2} py={2}
isDisabled={!value}
onClick={async () => { onClick={async () => {
if (!value) { if (!value) {
return onClose(); return onClose();
...@@ -54,6 +52,10 @@ const ImportSettings = ({ onClose }: Props) => { ...@@ -54,6 +52,10 @@ const ImportSettings = ({ onClose }: Props) => {
try { try {
const workflowConfig = parseWorkflowImportConfig({ const workflowConfig = parseWorkflowImportConfig({
config: JSON.parse(value), config: JSON.parse(value),
appType:
appType === AppTypeEnum.workflowTool
? AppTypeEnum.workflowTool
: AppTypeEnum.workflow,
t t
}); });
await removeUnauthModels({ modules: workflowConfig.nodes, allowedModels: myModels }); await removeUnauthModels({ modules: workflowConfig.nodes, allowedModels: myModels });
...@@ -65,7 +67,8 @@ const ImportSettings = ({ onClose }: Props) => { ...@@ -65,7 +67,8 @@ const ImportSettings = ({ onClose }: Props) => {
onClose(); onClose();
} catch { } catch {
toast({ toast({
title: t('app:import_configs_failed') title: t('app:import_configs_failed'),
status: 'error'
}); });
} }
}} }}
...@@ -73,7 +76,9 @@ const ImportSettings = ({ onClose }: Props) => { ...@@ -73,7 +76,9 @@ const ImportSettings = ({ onClose }: Props) => {
> >
{t('common:Save')} {t('common:Save')}
</Button> </Button>
</ModalFooter> }
>
<ImportAppConfigEditor value={value} onChange={setValue} rows={16} />
</MyModal> </MyModal>
); );
}; };
......
...@@ -134,7 +134,7 @@ const JsonImportModal = ({ scene, onClose }: JsonImportModalProps) => { ...@@ -134,7 +134,7 @@ const JsonImportModal = ({ scene, onClose }: JsonImportModalProps) => {
} }
}, [avatar, scene, workflowStr]); }, [avatar, scene, workflowStr]);
const { runAsync: onSubmit, loading: isCreating } = useRequest( const { run: onSubmit, loading: isCreating } = useRequest(
async ({ name, intro, workflowStr }: FormType) => { async ({ name, intro, workflowStr }: FormType) => {
if ((intro || '').length > 500) { if ((intro || '').length > 500) {
throw new Error(t('app:app_intro_too_long')); throw new Error(t('app:app_intro_too_long'));
...@@ -181,7 +181,7 @@ const JsonImportModal = ({ scene, onClose }: JsonImportModalProps) => { ...@@ -181,7 +181,7 @@ const JsonImportModal = ({ scene, onClose }: JsonImportModalProps) => {
<MyModal <MyModal
isOpen isOpen
onClose={handleCloseJsonImportModal} onClose={handleCloseJsonImportModal}
isLoading={isCreating || isFetching} isLoading={isFetching}
title={t('app:type.Import from json')} title={t('app:type.Import from json')}
size={'md'} size={'md'}
isCentered isCentered
...@@ -191,7 +191,11 @@ const JsonImportModal = ({ scene, onClose }: JsonImportModalProps) => { ...@@ -191,7 +191,11 @@ const JsonImportModal = ({ scene, onClose }: JsonImportModalProps) => {
<Button size={'md'} variant={'whiteBase'} onClick={handleCloseJsonImportModal}> <Button size={'md'} variant={'whiteBase'} onClick={handleCloseJsonImportModal}>
{t('common:Cancel')} {t('common:Cancel')}
</Button> </Button>
<Button size={'md'} onClick={handleSubmit(onSubmit)}> <Button
size={'md'}
isLoading={isCreating}
onClick={handleSubmit((data) => onSubmit(data))}
>
{t('common:Confirm')} {t('common:Confirm')}
</Button> </Button>
</> </>
......
...@@ -22,6 +22,14 @@ type ParsedImportConfig = { ...@@ -22,6 +22,14 @@ type ParsedImportConfig = {
type SupportedImportAppType = ParsedImportConfig['appType']; type SupportedImportAppType = ParsedImportConfig['appType'];
type ParseAppImportConfigOptions = {
config: unknown;
t: any;
resolveScene?: JsonImportModalScene;
allowedAppTypes?: readonly SupportedImportAppType[];
expectedAppType?: SupportedImportAppType;
};
const supportedImportAppTypes = [ const supportedImportAppTypes = [
AppTypeEnum.simple, AppTypeEnum.simple,
AppTypeEnum.workflow, AppTypeEnum.workflow,
...@@ -41,6 +49,16 @@ const isSupportedImportAppType = ( ...@@ -41,6 +49,16 @@ const isSupportedImportAppType = (
): type is (typeof supportedImportAppTypes)[number] => ): type is (typeof supportedImportAppTypes)[number] =>
supportedImportAppTypes.includes(type as (typeof supportedImportAppTypes)[number]); supportedImportAppTypes.includes(type as (typeof supportedImportAppTypes)[number]);
const importAppTypeAliasMap: Record<string, SupportedImportAppType> = {
workflowTool: AppTypeEnum.workflowTool
};
const normalizeImportAppType = (type: unknown): SupportedImportAppType | '' => {
if (isSupportedImportAppType(type)) return type;
if (typeof type === 'string') return importAppTypeAliasMap[type] || '';
return '';
};
export const isDashboardImportAppTypeAllowed = ({ export const isDashboardImportAppTypeAllowed = ({
appType, appType,
scene scene
...@@ -81,21 +99,51 @@ export const normalizeSimpleImportForm = (config: Record<string, unknown>) => { ...@@ -81,21 +99,51 @@ export const normalizeSimpleImportForm = (config: Record<string, unknown>) => {
return AppFormEditFormV1TypeSchema.safeParse(form); return AppFormEditFormV1TypeSchema.safeParse(form);
}; };
export const resolveImportAppType = (config: Record<string, unknown>) => { const assertImportConfigObject = (config: unknown, t: any): Record<string, unknown> => {
if (!config || typeof config !== 'object') {
throw new Error(t('app:type_not_recognized'));
}
return config as Record<string, unknown>;
};
export const resolveImportAppType = (
config: Record<string, unknown>,
scene?: JsonImportModalScene
) => {
const metaType = config.type; const metaType = config.type;
if (metaType !== undefined) { if (metaType !== undefined) {
if (!isSupportedImportAppType(metaType)) { const appType = normalizeImportAppType(metaType);
if (!appType) {
return ''; return '';
} }
return metaType; return appType;
} }
if ('nodes' in config && !Array.isArray(config.nodes)) { if ('nodes' in config && !Array.isArray(config.nodes)) {
return ''; return '';
} }
if (Array.isArray(config.nodes)) {
const hasPluginInputNode = config.nodes.some(
(node) =>
!!node &&
typeof node === 'object' &&
(node as { flowNodeType?: unknown }).flowNodeType === 'pluginInput'
);
const hasWorkflowStartNode = config.nodes.some(
(node) =>
!!node &&
typeof node === 'object' &&
(node as { flowNodeType?: unknown }).flowNodeType === 'workflowStart'
);
if (scene === 'tool' && hasPluginInputNode) return AppTypeEnum.workflowTool;
if (scene === 'agent' && hasWorkflowStartNode) return AppTypeEnum.workflow;
}
try { try {
return getAppType(config as any); return getAppType(config as any);
} catch { } catch {
...@@ -103,62 +151,64 @@ export const resolveImportAppType = (config: Record<string, unknown>) => { ...@@ -103,62 +151,64 @@ export const resolveImportAppType = (config: Record<string, unknown>) => {
} }
}; };
/** const assertImportAppTypeAllowed = ({
* 解析工作台 JSON 导入配置。 appType,
* allowedAppTypes,
* 顶层 `type` 存在时按导出元信息校验业务结构;无 `type` 时回退 expectedAppType,
* 现有 `getAppType` 结构识别逻辑,以兼容老版本导出 JSON。
*/
export const parseDashboardImportConfig = ({
config,
scene,
t t
}: { }: {
config: unknown; appType: SupportedImportAppType;
scene: JsonImportModalScene; allowedAppTypes?: readonly SupportedImportAppType[];
expectedAppType?: SupportedImportAppType;
t: any; t: any;
}): ParsedImportConfig => { }) => {
if (!config || typeof config !== 'object') { if (expectedAppType && appType !== expectedAppType) {
throw new Error(t('app:type_not_recognized'));
}
const workflowConfig = config as Record<string, unknown>;
const appType = resolveImportAppType(config as Record<string, unknown>);
if (!appType) {
throw new Error(t('app:type_not_recognized')); throw new Error(t('app:type_not_recognized'));
} }
if (!isDashboardImportAppTypeAllowed({ appType, scene })) { if (allowedAppTypes && !allowedAppTypes.includes(appType)) {
throw new Error(t('app:type_not_recognized')); throw new Error(t('app:type_not_recognized'));
} }
};
if (appType === AppTypeEnum.simple) { const parseSimpleImportWorkflow = ({
config,
t
}: {
config: Record<string, unknown>;
t: any;
}): ImportWorkflowConfig => {
if ( if (
!workflowConfig.aiSettings || !config.aiSettings ||
typeof workflowConfig.aiSettings !== 'object' || typeof config.aiSettings !== 'object' ||
Array.isArray(workflowConfig.aiSettings) Array.isArray(config.aiSettings)
) { ) {
throw new Error(t('app:type_not_recognized')); throw new Error(t('app:type_not_recognized'));
} }
const parsedForm = normalizeSimpleImportForm(workflowConfig); const parsedForm = normalizeSimpleImportForm(config);
if (!parsedForm.success) { if (!parsedForm.success) {
throw new Error(t('app:type_not_recognized')); throw new Error(t('app:type_not_recognized'));
} }
return { return form2AppWorkflow(parsedForm.data as AppFormEditFormType, t);
workflow: form2AppWorkflow(parsedForm.data as AppFormEditFormType, t), };
appType
};
}
if (!Array.isArray(workflowConfig.nodes)) { const parseWorkflowLikeImportConfig = ({
config,
appType,
t
}: {
config: Record<string, unknown>;
appType: Exclude<SupportedImportAppType, AppTypeEnum.simple>;
t: any;
}): ImportWorkflowConfig => {
if (!Array.isArray(config.nodes)) {
throw new Error(t('app:type_not_recognized')); throw new Error(t('app:type_not_recognized'));
} }
const matchedStartNodeType = appType === AppTypeEnum.workflow ? 'workflowStart' : 'pluginInput'; const matchedStartNodeType = appType === AppTypeEnum.workflow ? 'workflowStart' : 'pluginInput';
const hasMatchedStartNode = workflowConfig.nodes.some( const hasMatchedStartNode = config.nodes.some(
(node) => (node) =>
!!node && !!node &&
typeof node === 'object' && typeof node === 'object' &&
...@@ -170,29 +220,91 @@ export const parseDashboardImportConfig = ({ ...@@ -170,29 +220,91 @@ export const parseDashboardImportConfig = ({
} }
return { return {
workflow: { nodes: config.nodes as StoreNodeItemType[],
nodes: workflowConfig.nodes as StoreNodeItemType[], edges: Array.isArray(config.edges) ? (config.edges as StoreEdgeItemType[]) : [],
edges: Array.isArray(workflowConfig.edges) chatConfig: (config.chatConfig || {}) as AppChatConfigType
? (workflowConfig.edges as StoreEdgeItemType[])
: [],
chatConfig: (workflowConfig.chatConfig || {}) as AppChatConfigType
},
appType
}; };
}; };
/** /**
* 解析工作流详情内的 JSON 导入配置 * 统一解析导入 JSON
* *
* 该入口只允许导入 workflow 配置。导出的 `name`、`intro` 等应用元信息 * 这个入口只处理通用导入流程:识别导入类型、应用调用方约束、按类型解析为
* 只用于工作台新建应用,工作流内部导入时会忽略。 * workflow 数据。工作台导入和详情页导入只负责传入不同约束,避免把场景规则
* 和结构解析散落在多个入口里。
*/ */
export const parseWorkflowImportConfig = ({ config, t }: { config: unknown; t: any }) => { export const parseAppImportConfig = ({
const { workflow, appType } = parseDashboardImportConfig({ config, scene: 'agent', t }); config,
t,
resolveScene,
allowedAppTypes,
expectedAppType
}: ParseAppImportConfigOptions): ParsedImportConfig => {
const importConfig = assertImportConfigObject(config, t);
const appType = resolveImportAppType(importConfig, resolveScene);
if (appType !== AppTypeEnum.workflow) { if (!appType) {
throw new Error(t('app:type_not_recognized')); throw new Error(t('app:type_not_recognized'));
} }
assertImportAppTypeAllowed({ appType, allowedAppTypes, expectedAppType, t });
return {
workflow:
appType === AppTypeEnum.simple
? parseSimpleImportWorkflow({ config: importConfig, t })
: parseWorkflowLikeImportConfig({ config: importConfig, appType, t }),
appType
};
};
/**
* 解析工作台 JSON 导入配置。
*
* 顶层 `type` 存在时按导出元信息校验业务结构;无 `type` 时回退
* 现有 `getAppType` 结构识别逻辑,以兼容老版本导出 JSON。
*/
export const parseDashboardImportConfig = ({
config,
scene,
t
}: {
config: unknown;
scene: JsonImportModalScene;
t: any;
}): ParsedImportConfig => {
return parseAppImportConfig({
config,
t,
resolveScene: scene,
allowedAppTypes: dashboardImportAppTypesByScene[scene]
});
};
/**
* 解析应用详情内的 JSON 导入配置。
*
* 详情页导入只允许覆盖当前应用同类型的编排配置,避免普通工作流与插件工作流
* 互相导入后缺少各自的入口节点。导出的 `name`、`intro` 等应用元信息只用于
* 工作台新建应用,详情内导入时会忽略。
*/
export const parseWorkflowImportConfig = ({
config,
appType: expectedAppType = AppTypeEnum.workflow,
t
}: {
config: unknown;
appType?: AppTypeEnum.workflow | AppTypeEnum.workflowTool;
t: any;
}) => {
const scene: JsonImportModalScene =
expectedAppType === AppTypeEnum.workflowTool ? 'tool' : 'agent';
const { workflow } = parseAppImportConfig({
config,
t,
resolveScene: scene,
expectedAppType
});
return workflow; return workflow;
}; };
...@@ -17,6 +17,7 @@ vi.mock('@/pageComponents/app/detail/Edit/SimpleApp/utils', () => ({ ...@@ -17,6 +17,7 @@ vi.mock('@/pageComponents/app/detail/Edit/SimpleApp/utils', () => ({
const { const {
normalizeSimpleImportForm, normalizeSimpleImportForm,
parseAppImportConfig,
parseDashboardImportConfig, parseDashboardImportConfig,
parseWorkflowImportConfig, parseWorkflowImportConfig,
resolveImportAppType, resolveImportAppType,
...@@ -87,6 +88,10 @@ describe('resolveImportAppType', () => { ...@@ -87,6 +88,10 @@ describe('resolveImportAppType', () => {
expect(resolveImportAppType({ type: 'workflow' })).toBe(''); expect(resolveImportAppType({ type: 'workflow' })).toBe('');
}); });
it('should support legacy workflowTool type alias', () => {
expect(resolveImportAppType({ type: 'workflowTool' })).toBe(AppTypeEnum.workflowTool);
});
it('should fall back to old JSON structure detection when type is missing', () => { it('should fall back to old JSON structure detection when type is missing', () => {
expect(resolveImportAppType(createSimpleConfig())).toBe(AppTypeEnum.simple); expect(resolveImportAppType(createSimpleConfig())).toBe(AppTypeEnum.simple);
expect( expect(
...@@ -103,6 +108,16 @@ describe('resolveImportAppType', () => { ...@@ -103,6 +108,16 @@ describe('resolveImportAppType', () => {
).toBe(AppTypeEnum.workflowTool); ).toBe(AppTypeEnum.workflowTool);
}); });
it('should prefer plugin input in tool scene for old mixed workflow JSON', () => {
const config = {
nodes: [{ flowNodeType: 'workflowStart' }, { flowNodeType: 'pluginInput' }],
edges: []
};
expect(resolveImportAppType(config, 'tool')).toBe(AppTypeEnum.workflowTool);
expect(resolveImportAppType(config, 'agent')).toBe(AppTypeEnum.workflow);
});
it('should return empty type for malformed node list items', () => { it('should return empty type for malformed node list items', () => {
expect( expect(
resolveImportAppType({ resolveImportAppType({
...@@ -169,6 +184,33 @@ describe('parseDashboardImportConfig', () => { ...@@ -169,6 +184,33 @@ describe('parseDashboardImportConfig', () => {
expect(result.appType).toBe(AppTypeEnum.workflowTool); expect(result.appType).toBe(AppTypeEnum.workflowTool);
}); });
it('should parse legacy workflowTool alias JSON in tool dashboard', () => {
const result = parseDashboardImportConfig({
config: {
type: 'workflowTool',
nodes: [{ flowNodeType: 'pluginInput' }],
edges: []
},
scene: 'tool',
t
});
expect(result.appType).toBe(AppTypeEnum.workflowTool);
});
it('should parse old mixed workflow JSON as workflow tool in tool dashboard', () => {
const result = parseDashboardImportConfig({
config: {
nodes: [{ flowNodeType: 'workflowStart' }, { flowNodeType: 'pluginInput' }],
edges: []
},
scene: 'tool',
t
});
expect(result.appType).toBe(AppTypeEnum.workflowTool);
});
it('should reject workflow tool JSON in agent dashboard', () => { it('should reject workflow tool JSON in agent dashboard', () => {
expect(() => expect(() =>
parseDashboardImportConfig({ parseDashboardImportConfig({
...@@ -275,12 +317,51 @@ describe('isDashboardImportAppTypeAllowed', () => { ...@@ -275,12 +317,51 @@ describe('isDashboardImportAppTypeAllowed', () => {
expect( expect(
isDashboardImportAppTypeAllowed({ appType: AppTypeEnum.workflowTool, scene: 'agent' }) isDashboardImportAppTypeAllowed({ appType: AppTypeEnum.workflowTool, scene: 'agent' })
).toBe(false); ).toBe(false);
expect(isDashboardImportAppTypeAllowed({ appType: AppTypeEnum.simple, scene: 'tool' })).toBe(
false
);
expect(isDashboardImportAppTypeAllowed({ appType: AppTypeEnum.workflow, scene: 'tool' })).toBe(
false
);
expect( expect(
isDashboardImportAppTypeAllowed({ appType: AppTypeEnum.workflowTool, scene: 'tool' }) isDashboardImportAppTypeAllowed({ appType: AppTypeEnum.workflowTool, scene: 'tool' })
).toBe(true); ).toBe(true);
}); });
}); });
describe('parseAppImportConfig', () => {
it('should parse through the shared import entry with caller constraints', () => {
const result = parseAppImportConfig({
config: {
type: 'workflowTool',
nodes: [{ flowNodeType: 'pluginInput' }],
edges: []
},
resolveScene: 'tool',
expectedAppType: AppTypeEnum.workflowTool,
t
});
expect(result.appType).toBe(AppTypeEnum.workflowTool);
expect(result.workflow.nodes).toEqual([{ flowNodeType: 'pluginInput' }]);
});
it('should reject mismatched app type in the shared import entry', () => {
expect(() =>
parseAppImportConfig({
config: {
type: AppTypeEnum.workflow,
nodes: [{ flowNodeType: 'workflowStart' }],
edges: []
},
resolveScene: 'agent',
expectedAppType: AppTypeEnum.workflowTool,
t
})
).toThrow('app:type_not_recognized');
});
});
describe('parseWorkflowImportConfig', () => { describe('parseWorkflowImportConfig', () => {
it('should parse workflow JSON and ignore app meta in workflow detail import', () => { it('should parse workflow JSON and ignore app meta in workflow detail import', () => {
const result = parseWorkflowImportConfig({ const result = parseWorkflowImportConfig({
...@@ -302,7 +383,28 @@ describe('parseWorkflowImportConfig', () => { ...@@ -302,7 +383,28 @@ describe('parseWorkflowImportConfig', () => {
}); });
}); });
it('should reject non-workflow JSON in workflow detail import', () => { it('should parse workflow tool JSON in workflow tool detail import', () => {
const result = parseWorkflowImportConfig({
config: {
type: AppTypeEnum.workflowTool,
name: 'Tool name',
intro: 'Tool intro',
nodes: [{ flowNodeType: 'pluginInput' }, { flowNodeType: 'pluginOutput' }],
edges: [],
chatConfig: { welcomeText: 'plugin hello' }
},
appType: AppTypeEnum.workflowTool,
t
});
expect(result).toEqual({
nodes: [{ flowNodeType: 'pluginInput' }, { flowNodeType: 'pluginOutput' }],
edges: [],
chatConfig: { welcomeText: 'plugin hello' }
});
});
it('should reject mismatched JSON in workflow detail import', () => {
expect(() => expect(() =>
parseWorkflowImportConfig({ parseWorkflowImportConfig({
config: { config: {
...@@ -316,6 +418,18 @@ describe('parseWorkflowImportConfig', () => { ...@@ -316,6 +418,18 @@ describe('parseWorkflowImportConfig', () => {
expect(() => expect(() =>
parseWorkflowImportConfig({ parseWorkflowImportConfig({
config: {
type: AppTypeEnum.workflow,
nodes: [{ flowNodeType: 'workflowStart' }],
edges: []
},
appType: AppTypeEnum.workflowTool,
t
})
).toThrow('app:type_not_recognized');
expect(() =>
parseWorkflowImportConfig({
config: createSimpleConfig({ config: createSimpleConfig({
type: AppTypeEnum.simple type: AppTypeEnum.simple
}), }),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment