From 0ef3d40296eae0e848d4e56bebc1aee44090940c Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Mon, 12 May 2025 22:27:01 +0800 Subject: [PATCH] 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 --- .../zh-cn/docs/development/upgrading/498.md | 2 + packages/global/core/ai/prompt/AIChat.ts | 2 +- packages/global/core/ai/prompt/utils.ts | 3 +- .../global/core/workflow/node/constant.ts | 8 +- .../global/core/workflow/runtime/type.d.ts | 2 +- .../template/system/abandoned/runApp/index.ts | 1 - .../workflow/template/system/aiChat/index.ts | 8 +- .../template/system/assignedAnswer.ts | 1 - .../template/system/customFeedback.ts | 1 - .../workflow/template/system/datasetConcat.ts | 1 - .../core/workflow/template/system/http468.ts | 1 - .../workflow/template/system/ifElse/index.ts | 1 - .../template/system/interactive/formInput.ts | 1 - .../template/system/interactive/userSelect.ts | 1 - .../core/workflow/template/system/laf.ts | 1 - .../workflow/template/system/loop/loop.ts | 1 - .../workflow/template/system/loop/loopEnd.ts | 1 - .../template/system/loop/loopStart.ts | 1 - .../workflow/template/system/pluginConfig.ts | 1 - .../workflow/template/system/pluginInput.ts | 1 - .../workflow/template/system/pluginOutput.ts | 1 - .../core/workflow/template/system/runApp.ts | 1 - .../workflow/template/system/runPlugin.ts | 1 - .../core/workflow/template/system/runTool.ts | 1 - .../workflow/template/system/runToolSet.ts | 1 - .../workflow/template/system/sandbox/index.ts | 1 - .../core/workflow/template/system/stopTool.ts | 1 - .../workflow/template/system/systemConfig.ts | 1 - .../workflow/template/system/textEditor.ts | 1 - .../workflow/template/system/toolParams.ts | 1 - .../template/system/variableUpdate/index.tsx | 1 - .../workflow/template/system/workflowStart.ts | 1 - packages/global/core/workflow/type/index.d.ts | 5 +- packages/global/core/workflow/type/io.d.ts | 4 + packages/global/core/workflow/type/node.d.ts | 7 +- .../service/core/app/plugin/controller.ts | 46 ++- packages/service/core/app/utils.ts | 47 ++- .../service/core/app/version/controller.ts | 19 + .../dispatch/agent/runTool/toolChoice.ts | 4 +- .../core/workflow/dispatch/chat/oneapi.ts | 2 +- .../service/core/workflow/dispatch/index.ts | 3 +- .../web/components/common/MySelect/index.tsx | 129 ++++--- packages/web/hooks/useScrollPagination.tsx | 4 +- packages/web/i18n/en/app.json | 5 +- packages/web/i18n/en/common.json | 1 + packages/web/i18n/zh-CN/app.json | 5 +- packages/web/i18n/zh-CN/common.json | 1 + packages/web/i18n/zh-Hant/app.json | 5 +- packages/web/i18n/zh-Hant/common.json | 1 + .../app/detail/PublishHistoriesSlider.tsx | 4 +- .../Flow/NodeTemplatesModal.tsx | 45 +-- .../Flow/nodes/render/NodeCard.tsx | 199 +++++----- .../Flow/nodes/render/RenderInput/Label.tsx | 34 +- .../Flow/nodes/render/RenderOutput/Label.tsx | 36 ++ projects/app/src/pages/api/core/app/create.ts | 4 +- .../api/core/app/plugin/getPreviewNode.ts | 6 +- .../src/pages/api/core/app/version/list.ts | 14 +- projects/app/src/service/core/app/utils.ts | 5 +- projects/app/src/web/core/app/api/plugin.ts | 4 +- projects/app/src/web/core/app/api/version.ts | 2 +- projects/app/src/web/core/app/utils.ts | 2 - projects/app/src/web/core/workflow/utils.ts | 33 +- .../workflow/store2flow.deprecated.test.ts | 86 +++++ .../web/workflow/store2flow.version.test.ts | 113 ++++++ .../app/test/cases/web/workflow/utils.test.ts | 339 ++++++++++++++++++ scripts/openapi/openapi.ts | 2 +- .../api/support/mcp/server/toolList.test.ts | 14 +- test/cases/web/core/app/utils.test.ts | 105 ------ test/cases/web/core/workflow/utils.test.ts | 237 ------------ 69 files changed, 1024 insertions(+), 599 deletions(-) create mode 100644 projects/app/test/cases/web/workflow/store2flow.deprecated.test.ts create mode 100644 projects/app/test/cases/web/workflow/store2flow.version.test.ts create mode 100644 projects/app/test/cases/web/workflow/utils.test.ts delete mode 100644 test/cases/web/core/app/utils.test.ts delete mode 100644 test/cases/web/core/workflow/utils.test.ts diff --git a/docSite/content/zh-cn/docs/development/upgrading/498.md b/docSite/content/zh-cn/docs/development/upgrading/498.md index 0a2d702c8..d288914bb 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/498.md +++ b/docSite/content/zh-cn/docs/development/upgrading/498.md @@ -22,6 +22,7 @@ weight: 792 1. Chat log list 优化,避免大数据时超出内存限制。 2. 预加载 token 计算 worker,避免主任务中并发创建导致线程阻塞。 +3. 工作流节点版本控制交互优化。 ## 🐛 修复 @@ -31,4 +32,5 @@ weight: 792 4. 重新训练时,图片过期索引未成功清除,导致图片会丢失。 5. 重新训练权限问题。 6. 文档链接地址。 +7. Claude 工具调用,由于 index 为空,导致工具调用失败。 diff --git a/packages/global/core/ai/prompt/AIChat.ts b/packages/global/core/ai/prompt/AIChat.ts index d18153b4d..2c1c1a13e 100644 --- a/packages/global/core/ai/prompt/AIChat.ts +++ b/packages/global/core/ai/prompt/AIChat.ts @@ -331,7 +331,7 @@ export const getQuotePrompt = (version?: string, role: 'user' | 'system' = 'user }; // Document quote prompt -export const getDocumentQuotePrompt = (version: string) => { +export const getDocumentQuotePrompt = (version?: string) => { const promptMap = { ['4.9.2']: `将 中的内容作为本次对话的参考: diff --git a/packages/global/core/ai/prompt/utils.ts b/packages/global/core/ai/prompt/utils.ts index 624ec40b9..0cf1e1a4f 100644 --- a/packages/global/core/ai/prompt/utils.ts +++ b/packages/global/core/ai/prompt/utils.ts @@ -1,4 +1,5 @@ export const getPromptByVersion = (version?: string, promptMap: Record = {}) => { + // 版本号大的在前面 const versions = Object.keys(promptMap).sort((a, b) => { const [majorA, minorA, patchA] = a.split('.').map(Number); const [majorB, minorB, patchB] = b.split('.').map(Number); @@ -15,5 +16,5 @@ export const getPromptByVersion = (version?: string, promptMap: Record = { + [FlowNodeTypeEnum.pluginModule]: true, + [FlowNodeTypeEnum.appModule]: true, + [FlowNodeTypeEnum.tool]: true, + [FlowNodeTypeEnum.toolSet]: true +}; diff --git a/packages/global/core/workflow/runtime/type.d.ts b/packages/global/core/workflow/runtime/type.d.ts index ca04827f9..9880c0745 100644 --- a/packages/global/core/workflow/runtime/type.d.ts +++ b/packages/global/core/workflow/runtime/type.d.ts @@ -101,7 +101,7 @@ export type RuntimeNodeItemType = { outputs: FlowNodeOutputItemType[]; pluginId?: string; // workflow id / plugin id - version: string; + version?: string; }; export type RuntimeEdgeItemType = StoreEdgeItemType & { diff --git a/packages/global/core/workflow/template/system/abandoned/runApp/index.ts b/packages/global/core/workflow/template/system/abandoned/runApp/index.ts index 8f6a96209..6c8a78712 100644 --- a/packages/global/core/workflow/template/system/abandoned/runApp/index.ts +++ b/packages/global/core/workflow/template/system/abandoned/runApp/index.ts @@ -25,7 +25,6 @@ export const RunAppModule: FlowNodeTemplateType = { name: i18nT('workflow:application_call'), intro: i18nT('workflow:select_another_application_to_call'), showStatus: true, - version: '481', isTool: true, inputs: [ { diff --git a/packages/global/core/workflow/template/system/aiChat/index.ts b/packages/global/core/workflow/template/system/aiChat/index.ts index 02ab50a1e..1fbc53c58 100644 --- a/packages/global/core/workflow/template/system/aiChat/index.ts +++ b/packages/global/core/workflow/template/system/aiChat/index.ts @@ -19,7 +19,6 @@ import { Input_Template_UserChatInput, Input_Template_File_Link } from '../../input'; -import { chatNodeSystemPromptTip, systemPromptTip } from '../../tip'; import { getHandleConfig } from '../../utils'; import { i18nT } from '../../../../../../web/i18n/utils'; @@ -121,12 +120,7 @@ export const AiChatModule: FlowNodeTemplateType = { valueType: WorkflowIOValueTypeEnum.string }, // settings modal --- - { - ...Input_Template_System_Prompt, - label: i18nT('common:core.ai.Prompt'), - description: systemPromptTip, - placeholder: chatNodeSystemPromptTip - }, + Input_Template_System_Prompt, Input_Template_History, Input_Template_Dataset_Quote, Input_Template_File_Link, diff --git a/packages/global/core/workflow/template/system/assignedAnswer.ts b/packages/global/core/workflow/template/system/assignedAnswer.ts index bfdd4b2d5..b62009164 100644 --- a/packages/global/core/workflow/template/system/assignedAnswer.ts +++ b/packages/global/core/workflow/template/system/assignedAnswer.ts @@ -18,7 +18,6 @@ export const AssignedAnswerModule: FlowNodeTemplateType = { name: i18nT('workflow:assigned_reply'), intro: i18nT('workflow:intro_assigned_reply'), courseUrl: '/docs/guide/dashboard/workflow/reply/', - version: '481', isTool: true, inputs: [ { diff --git a/packages/global/core/workflow/template/system/customFeedback.ts b/packages/global/core/workflow/template/system/customFeedback.ts index 71675846a..f5f3be338 100644 --- a/packages/global/core/workflow/template/system/customFeedback.ts +++ b/packages/global/core/workflow/template/system/customFeedback.ts @@ -18,7 +18,6 @@ export const CustomFeedbackNode: FlowNodeTemplateType = { name: i18nT('workflow:custom_feedback'), intro: i18nT('workflow:intro_custom_feedback'), courseUrl: '/docs/guide/dashboard/workflow/custom_feedback/', - version: '486', inputs: [ { key: NodeInputKeyEnum.textareaInput, diff --git a/packages/global/core/workflow/template/system/datasetConcat.ts b/packages/global/core/workflow/template/system/datasetConcat.ts index 43f9147b8..5f171f57a 100644 --- a/packages/global/core/workflow/template/system/datasetConcat.ts +++ b/packages/global/core/workflow/template/system/datasetConcat.ts @@ -42,7 +42,6 @@ export const DatasetConcatModule: FlowNodeTemplateType = { intro: i18nT('workflow:intro_knowledge_base_search_merge'), showStatus: false, - version: '486', courseUrl: '/docs/guide/dashboard/workflow/knowledge_base_search_merge/', inputs: [ { diff --git a/packages/global/core/workflow/template/system/http468.ts b/packages/global/core/workflow/template/system/http468.ts index 19d8c5dc8..adeaec6f9 100644 --- a/packages/global/core/workflow/template/system/http468.ts +++ b/packages/global/core/workflow/template/system/http468.ts @@ -28,7 +28,6 @@ export const HttpNode468: FlowNodeTemplateType = { showStatus: true, isTool: true, courseUrl: '/docs/guide/dashboard/workflow/http/', - version: '481', inputs: [ { ...Input_Template_DynamicInput, diff --git a/packages/global/core/workflow/template/system/ifElse/index.ts b/packages/global/core/workflow/template/system/ifElse/index.ts index 42c1e3d86..237823552 100644 --- a/packages/global/core/workflow/template/system/ifElse/index.ts +++ b/packages/global/core/workflow/template/system/ifElse/index.ts @@ -24,7 +24,6 @@ export const IfElseNode: FlowNodeTemplateType = { intro: i18nT('workflow:execute_different_branches_based_on_conditions'), showStatus: true, courseUrl: '/docs/guide/dashboard/workflow/tfswitch/', - version: '481', inputs: [ { key: NodeInputKeyEnum.ifElseList, diff --git a/packages/global/core/workflow/template/system/interactive/formInput.ts b/packages/global/core/workflow/template/system/interactive/formInput.ts index e5924bdb4..5487f6f16 100644 --- a/packages/global/core/workflow/template/system/interactive/formInput.ts +++ b/packages/global/core/workflow/template/system/interactive/formInput.ts @@ -23,7 +23,6 @@ export const FormInputNode: FlowNodeTemplateType = { name: i18nT('app:workflow.form_input'), intro: i18nT(`app:workflow.form_input_tip`), isTool: true, - version: '4811', inputs: [ { key: NodeInputKeyEnum.description, diff --git a/packages/global/core/workflow/template/system/interactive/userSelect.ts b/packages/global/core/workflow/template/system/interactive/userSelect.ts index 7da84af57..723cffa33 100644 --- a/packages/global/core/workflow/template/system/interactive/userSelect.ts +++ b/packages/global/core/workflow/template/system/interactive/userSelect.ts @@ -24,7 +24,6 @@ export const UserSelectNode: FlowNodeTemplateType = { name: i18nT('app:workflow.user_select'), intro: i18nT(`app:workflow.user_select_tip`), isTool: true, - version: '489', courseUrl: '/docs/guide/dashboard/workflow/user-selection/', inputs: [ { diff --git a/packages/global/core/workflow/template/system/laf.ts b/packages/global/core/workflow/template/system/laf.ts index 275c81bc0..c9d886eb7 100644 --- a/packages/global/core/workflow/template/system/laf.ts +++ b/packages/global/core/workflow/template/system/laf.ts @@ -33,7 +33,6 @@ export const LafModule: FlowNodeTemplateType = { showStatus: true, isTool: true, courseUrl: '/docs/guide/dashboard/workflow/laf/', - version: '481', inputs: [ { ...Input_Template_DynamicInput, diff --git a/packages/global/core/workflow/template/system/loop/loop.ts b/packages/global/core/workflow/template/system/loop/loop.ts index 7ed289f3e..16a5c75e1 100644 --- a/packages/global/core/workflow/template/system/loop/loop.ts +++ b/packages/global/core/workflow/template/system/loop/loop.ts @@ -29,7 +29,6 @@ export const LoopNode: FlowNodeTemplateType = { name: i18nT('workflow:loop'), intro: i18nT('workflow:intro_loop'), showStatus: true, - version: '4811', courseUrl: '/docs/guide/dashboard/workflow/loop/', inputs: [ { diff --git a/packages/global/core/workflow/template/system/loop/loopEnd.ts b/packages/global/core/workflow/template/system/loop/loopEnd.ts index 51ec730b8..9712ac8b5 100644 --- a/packages/global/core/workflow/template/system/loop/loopEnd.ts +++ b/packages/global/core/workflow/template/system/loop/loopEnd.ts @@ -19,7 +19,6 @@ export const LoopEndNode: FlowNodeTemplateType = { avatar: 'core/workflow/template/loopEnd', name: i18nT('workflow:loop_end'), showStatus: false, - version: '4811', inputs: [ { key: NodeInputKeyEnum.loopEndInput, diff --git a/packages/global/core/workflow/template/system/loop/loopStart.ts b/packages/global/core/workflow/template/system/loop/loopStart.ts index 0e9abb1a2..c355b8af9 100644 --- a/packages/global/core/workflow/template/system/loop/loopStart.ts +++ b/packages/global/core/workflow/template/system/loop/loopStart.ts @@ -24,7 +24,6 @@ export const LoopStartNode: FlowNodeTemplateType = { unique: true, forbidDelete: true, showStatus: false, - version: '4811', inputs: [ { key: NodeInputKeyEnum.loopStartInput, diff --git a/packages/global/core/workflow/template/system/pluginConfig.ts b/packages/global/core/workflow/template/system/pluginConfig.ts index 07200d229..5b5960c67 100644 --- a/packages/global/core/workflow/template/system/pluginConfig.ts +++ b/packages/global/core/workflow/template/system/pluginConfig.ts @@ -15,7 +15,6 @@ export const PluginConfigNode: FlowNodeTemplateType = { intro: '', unique: true, forbidDelete: true, - version: '4811', inputs: [], outputs: [] }; diff --git a/packages/global/core/workflow/template/system/pluginInput.ts b/packages/global/core/workflow/template/system/pluginInput.ts index e79312b3a..262c810a0 100644 --- a/packages/global/core/workflow/template/system/pluginInput.ts +++ b/packages/global/core/workflow/template/system/pluginInput.ts @@ -16,7 +16,6 @@ export const PluginInputModule: FlowNodeTemplateType = { name: i18nT('workflow:plugin_input'), intro: i18nT('workflow:intro_plugin_input'), showStatus: false, - version: '481', inputs: [], outputs: [] }; diff --git a/packages/global/core/workflow/template/system/pluginOutput.ts b/packages/global/core/workflow/template/system/pluginOutput.ts index c2d6cfc7a..b5cb49eb3 100644 --- a/packages/global/core/workflow/template/system/pluginOutput.ts +++ b/packages/global/core/workflow/template/system/pluginOutput.ts @@ -16,7 +16,6 @@ export const PluginOutputModule: FlowNodeTemplateType = { name: i18nT('workflow:template.plugin_output'), intro: i18nT('workflow:intro_custom_plugin_output'), showStatus: false, - version: '481', inputs: [], outputs: [] }; diff --git a/packages/global/core/workflow/template/system/runApp.ts b/packages/global/core/workflow/template/system/runApp.ts index f6434d4a2..cf8a8534c 100644 --- a/packages/global/core/workflow/template/system/runApp.ts +++ b/packages/global/core/workflow/template/system/runApp.ts @@ -13,7 +13,6 @@ export const RunAppNode: FlowNodeTemplateType = { name: '', showStatus: false, isTool: false, - version: '481', inputs: [], // [{key:'pluginId'},...] outputs: [] }; diff --git a/packages/global/core/workflow/template/system/runPlugin.ts b/packages/global/core/workflow/template/system/runPlugin.ts index 6bbc9ee1a..1ee726d09 100644 --- a/packages/global/core/workflow/template/system/runPlugin.ts +++ b/packages/global/core/workflow/template/system/runPlugin.ts @@ -13,7 +13,6 @@ export const RunPluginModule: FlowNodeTemplateType = { name: '', showStatus: false, isTool: true, - version: '481', inputs: [], // [{key:'pluginId'},...] outputs: [] }; diff --git a/packages/global/core/workflow/template/system/runTool.ts b/packages/global/core/workflow/template/system/runTool.ts index 97c206858..60df16363 100644 --- a/packages/global/core/workflow/template/system/runTool.ts +++ b/packages/global/core/workflow/template/system/runTool.ts @@ -13,7 +13,6 @@ export const RunToolNode: FlowNodeTemplateType = { name: '', showStatus: false, isTool: true, - version: '4.9.6', inputs: [], outputs: [] }; diff --git a/packages/global/core/workflow/template/system/runToolSet.ts b/packages/global/core/workflow/template/system/runToolSet.ts index c935cd677..71aefac76 100644 --- a/packages/global/core/workflow/template/system/runToolSet.ts +++ b/packages/global/core/workflow/template/system/runToolSet.ts @@ -13,7 +13,6 @@ export const RunToolSetNode: FlowNodeTemplateType = { name: '', showStatus: false, isTool: true, - version: '4.9.6', inputs: [], outputs: [] }; diff --git a/packages/global/core/workflow/template/system/sandbox/index.ts b/packages/global/core/workflow/template/system/sandbox/index.ts index eeeadcc52..cc7203fc2 100644 --- a/packages/global/core/workflow/template/system/sandbox/index.ts +++ b/packages/global/core/workflow/template/system/sandbox/index.ts @@ -27,7 +27,6 @@ export const CodeNode: FlowNodeTemplateType = { intro: i18nT('workflow:execute_a_simple_script_code_usually_for_complex_data_processing'), showStatus: true, courseUrl: '/docs/guide/dashboard/workflow/sandbox/', - version: '482', inputs: [ { ...Input_Template_DynamicInput, diff --git a/packages/global/core/workflow/template/system/stopTool.ts b/packages/global/core/workflow/template/system/stopTool.ts index c4cdd63a6..e2e7cb36b 100644 --- a/packages/global/core/workflow/template/system/stopTool.ts +++ b/packages/global/core/workflow/template/system/stopTool.ts @@ -13,7 +13,6 @@ export const StopToolNode: FlowNodeTemplateType = { avatar: 'core/workflow/template/stopTool', name: i18nT('workflow:tool_call_termination'), intro: i18nT('workflow:intro_tool_call_termination'), - version: '481', inputs: [], outputs: [] }; diff --git a/packages/global/core/workflow/template/system/systemConfig.ts b/packages/global/core/workflow/template/system/systemConfig.ts index fbfc3483e..e9c3efcd0 100644 --- a/packages/global/core/workflow/template/system/systemConfig.ts +++ b/packages/global/core/workflow/template/system/systemConfig.ts @@ -15,7 +15,6 @@ export const SystemConfigNode: FlowNodeTemplateType = { intro: '', unique: true, forbidDelete: true, - version: '481', inputs: [], outputs: [] }; diff --git a/packages/global/core/workflow/template/system/textEditor.ts b/packages/global/core/workflow/template/system/textEditor.ts index eed29757a..7faf6e5fe 100644 --- a/packages/global/core/workflow/template/system/textEditor.ts +++ b/packages/global/core/workflow/template/system/textEditor.ts @@ -24,7 +24,6 @@ export const TextEditorNode: FlowNodeTemplateType = { name: i18nT('workflow:text_concatenation'), intro: i18nT('workflow:intro_text_concatenation'), courseUrl: '/docs/guide/dashboard/workflow/text_editor/', - version: '4813', inputs: [ { key: NodeInputKeyEnum.textareaInput, diff --git a/packages/global/core/workflow/template/system/toolParams.ts b/packages/global/core/workflow/template/system/toolParams.ts index cd35f0783..22267a59c 100644 --- a/packages/global/core/workflow/template/system/toolParams.ts +++ b/packages/global/core/workflow/template/system/toolParams.ts @@ -13,7 +13,6 @@ export const ToolParamsNode: FlowNodeTemplateType = { avatar: 'core/workflow/template/toolParams', name: i18nT('workflow:tool_custom_field'), intro: i18nT('workflow:intro_tool_params_config'), - version: '4811', isTool: true, inputs: [], outputs: [] diff --git a/packages/global/core/workflow/template/system/variableUpdate/index.tsx b/packages/global/core/workflow/template/system/variableUpdate/index.tsx index 4f90168f2..50206246f 100644 --- a/packages/global/core/workflow/template/system/variableUpdate/index.tsx +++ b/packages/global/core/workflow/template/system/variableUpdate/index.tsx @@ -19,7 +19,6 @@ export const VariableUpdateNode: FlowNodeTemplateType = { intro: i18nT('workflow:update_specified_node_output_or_global_variable'), showStatus: false, isTool: true, - version: '481', courseUrl: '/docs/guide/dashboard/workflow/variable_update/', inputs: [ { diff --git a/packages/global/core/workflow/template/system/workflowStart.ts b/packages/global/core/workflow/template/system/workflowStart.ts index 57837e06a..54b1dbb63 100644 --- a/packages/global/core/workflow/template/system/workflowStart.ts +++ b/packages/global/core/workflow/template/system/workflowStart.ts @@ -30,7 +30,6 @@ export const WorkflowStart: FlowNodeTemplateType = { intro: '', forbidDelete: true, unique: true, - version: '481', inputs: [{ ...Input_Template_UserChatInput, toolDescription: i18nT('workflow:user_question') }], outputs: [ { diff --git a/packages/global/core/workflow/type/index.d.ts b/packages/global/core/workflow/type/index.d.ts index c8b641f13..2bfa1787b 100644 --- a/packages/global/core/workflow/type/index.d.ts +++ b/packages/global/core/workflow/type/index.d.ts @@ -37,7 +37,10 @@ export type WorkflowTemplateType = { intro?: string; author?: string; courseUrl?: string; - version: string; + + version?: string; + versionLabel?: string; + isLatestVersion?: boolean; showStatus?: boolean; weight?: number; diff --git a/packages/global/core/workflow/type/io.d.ts b/packages/global/core/workflow/type/io.d.ts index de8332574..90bbf4a9d 100644 --- a/packages/global/core/workflow/type/io.d.ts +++ b/packages/global/core/workflow/type/io.d.ts @@ -63,6 +63,8 @@ export type FlowNodeInputItemType = InputComponentPropsType & { canSelectFile?: boolean; canSelectImg?: boolean; maxFiles?: number; + + deprecated?: boolean; }; export type FlowNodeOutputItemType = { @@ -86,6 +88,8 @@ export type FlowNodeOutputItemType = { // component params customFieldConfig?: CustomFieldConfigType; + + deprecated?: boolean; }; export type ReferenceItemValueType = [string, string | undefined]; diff --git a/packages/global/core/workflow/type/node.d.ts b/packages/global/core/workflow/type/node.d.ts index 007f23820..f27deb32f 100644 --- a/packages/global/core/workflow/type/node.d.ts +++ b/packages/global/core/workflow/type/node.d.ts @@ -34,7 +34,10 @@ export type FlowNodeCommonType = { name: string; intro?: string; // template list intro showStatus?: boolean; // chatting response step status - version: string; + + version?: string; + versionLabel?: string; // Just ui show + isLatestVersion?: boolean; // Just ui show // data inputs: FlowNodeInputItemType[]; @@ -48,7 +51,7 @@ export type FlowNodeCommonType = { }; export type PluginDataType = { - version: string; + version?: string; diagram?: string; userGuide?: string; courseUrl?: string; diff --git a/packages/service/core/app/plugin/controller.ts b/packages/service/core/app/plugin/controller.ts index b31a20b94..2237a75da 100644 --- a/packages/service/core/app/plugin/controller.ts +++ b/packages/service/core/app/plugin/controller.ts @@ -1,5 +1,5 @@ import { type FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node.d'; -import { FlowNodeTypeEnum, defaultNodeVersion } from '@fastgpt/global/core/workflow/node/constant'; +import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { appData2FlowNodeIO, pluginData2FlowNodeIO, @@ -14,10 +14,16 @@ import { cloneDeep } from 'lodash'; import { MongoApp } from '../schema'; import { type SystemPluginTemplateItemType } from '@fastgpt/global/core/workflow/type'; import { getSystemPluginTemplates } from '../../../../plugins/register'; -import { getAppLatestVersion, getAppVersionById } from '../version/controller'; +import { + checkIsLatestVersion, + getAppLatestVersion, + getAppVersionById +} from '../version/controller'; import { type PluginRuntimeType } from '@fastgpt/global/core/plugin/type'; import { MongoSystemPlugin } from './systemPluginSchema'; import { PluginErrEnum } from '@fastgpt/global/common/error/code/plugin'; +import { MongoAppVersion } from '../version/schema'; +import { i18nT } from '../../../../web/i18n/utils'; /* plugin id rule: @@ -90,20 +96,34 @@ const getSystemPluginTemplateById = async ( /* Format plugin to workflow preview node data */ export async function getChildAppPreviewNode({ - id + appId, + versionId }: { - id: string; + appId: string; + versionId?: string; }): Promise { const app: ChildAppType = await (async () => { - const { source, pluginId } = await splitCombinePluginId(id); + const { source, pluginId } = await splitCombinePluginId(appId); if (source === PluginSourceEnum.personal) { - const item = await MongoApp.findById(id).lean(); + const item = await MongoApp.findById(appId).lean(); if (!item) return Promise.reject('plugin not found'); - const version = await getAppLatestVersion(id, item); + const version = await getAppVersionById({ appId, versionId, app: item }); - if (!version.versionId) return Promise.reject('App version not found'); + if (!version.versionId) return Promise.reject(i18nT('common:app_not_version')); + + const versionData = await MongoAppVersion.findById( + version.versionId, + '_id versionName appId time' + ).lean(); + + const isLatest = versionData + ? await checkIsLatestVersion({ + appId, + versionId: versionData._id + }) + : true; return { id: String(item._id), @@ -118,7 +138,11 @@ export async function getChildAppPreviewNode({ chatConfig: version.chatConfig }, templateType: FlowNodeTemplateTypeEnum.teamApp, + version: version.versionId, + versionLabel: versionData?.versionName || '', + isLatestVersion: isLatest, + originCost: 0, currentCost: 0, hasTokenFee: false, @@ -175,7 +199,11 @@ export async function getChildAppPreviewNode({ userGuide: app.userGuide, showStatus: app.showStatus, isTool: true, + version: app.version, + versionLabel: app.versionLabel, + isLatestVersion: app.isLatestVersion, + sourceHandle: isToolSet ? getHandleConfig(false, false, false, false) : getHandleConfig(true, true, true, true), @@ -224,7 +252,7 @@ export async function getChildAppRuntimeById( templateType: FlowNodeTemplateTypeEnum.teamApp, // 用不到 - version: item?.pluginData?.nodeVersion || defaultNodeVersion, + version: item?.pluginData?.nodeVersion, originCost: 0, currentCost: 0, hasTokenFee: false, diff --git a/packages/service/core/app/utils.ts b/packages/service/core/app/utils.ts index 3c2359de8..91ce6404b 100644 --- a/packages/service/core/app/utils.ts +++ b/packages/service/core/app/utils.ts @@ -1,8 +1,14 @@ import { MongoDataset } from '../dataset/schema'; import { getEmbeddingModel } from '../ai/model'; -import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { + AppNodeFlowNodeTypeMap, + FlowNodeTypeEnum +} from '@fastgpt/global/core/workflow/node/constant'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; +import { MongoAppVersion } from './version/schema'; +import { checkIsLatestVersion } from './version/controller'; +import { Types } from '../../common/mongo'; export async function listAppDatasetDataByTeamIdAndDatasetIds({ teamId, @@ -35,6 +41,45 @@ export async function rewriteAppWorkflowToDetail({ }) { const datasetIdSet = new Set(); + // Add node(App Type) versionlabel and latest sign + const appNodes = nodes.filter((node) => AppNodeFlowNodeTypeMap[node.flowNodeType]); + const versionIds = appNodes + .filter((node) => node.version && Types.ObjectId.isValid(node.version)) + .map((node) => node.version); + if (versionIds.length > 0) { + const versionDataList = await MongoAppVersion.find( + { + _id: { $in: versionIds } + }, + '_id versionName appId time' + ).lean(); + + const versionMap: Record = {}; + + const isLatestChecks = await Promise.all( + versionDataList.map(async (version) => { + const isLatest = await checkIsLatestVersion({ + appId: version.appId, + versionId: version._id + }); + + return { versionId: String(version._id), isLatest }; + }) + ); + const isLatestMap = new Map(isLatestChecks.map((item) => [item.versionId, item.isLatest])); + versionDataList.forEach((version) => { + versionMap[String(version._id)] = version; + }); + appNodes.forEach((node) => { + if (!node.version) return; + const versionData = versionMap[String(node.version)]; + if (versionData) { + node.versionLabel = versionData.versionName; + node.isLatestVersion = isLatestMap.get(String(node.version)) || false; + } + }); + } + // Get all dataset ids from nodes nodes.forEach((node) => { if (node.flowNodeType !== FlowNodeTypeEnum.datasetSearchNode) return; diff --git a/packages/service/core/app/version/controller.ts b/packages/service/core/app/version/controller.ts index 365ae4a49..7ec631664 100644 --- a/packages/service/core/app/version/controller.ts +++ b/packages/service/core/app/version/controller.ts @@ -57,3 +57,22 @@ export const getAppVersionById = async ({ // If the version does not exist, the latest version is returned return getAppLatestVersion(appId, app); }; + +export const checkIsLatestVersion = async ({ + appId, + versionId +}: { + appId: string; + versionId: string; +}) => { + const version = await MongoAppVersion.findOne( + { + appId, + isPublish: true, + _id: { $gt: versionId } + }, + '_id' + ).lean(); + + return !version; +}; diff --git a/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts b/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts index 63675edb9..f85246c9f 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts @@ -723,8 +723,8 @@ async function streamResponse({ } // Parse tool calls if (responseChoice?.tool_calls?.length) { - responseChoice.tool_calls.forEach((toolCall) => { - const index = toolCall.index; + responseChoice.tool_calls.forEach((toolCall, i) => { + const index = toolCall.index ?? i; // Call new tool if (toolCall.id || callingTool) { diff --git a/packages/service/core/workflow/dispatch/chat/oneapi.ts b/packages/service/core/workflow/dispatch/chat/oneapi.ts index e34e9a49d..e9f1734c1 100644 --- a/packages/service/core/workflow/dispatch/chat/oneapi.ts +++ b/packages/service/core/workflow/dispatch/chat/oneapi.ts @@ -464,7 +464,7 @@ async function getChatMessages({ aiChatQuoteRole: AiChatQuoteRoleType; // user: replace user prompt; system: replace system prompt datasetQuotePrompt?: string; datasetQuoteText: string; - version: string; + version?: string; useDatasetQuote: boolean; histories: ChatItemType[]; diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index 73e994dad..ce0075f6e 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -11,6 +11,7 @@ import type { SystemVariablesType } from '@fastgpt/global/core/workflow/runtime/type'; import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type.d'; +import type { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io.d'; import type { AIChatItemValueItemType, ChatHistoryItemResType, @@ -549,7 +550,7 @@ export async function dispatchWorkFlow(data: Props): Promise = Omit & { value?: T; + valueLabel?: string | React.ReactNode; placeholder?: string; isSearch?: boolean; list: { - alias?: string; + alias?: string | React.ReactNode; icon?: string; iconSize?: string; label: string | React.ReactNode; @@ -49,18 +51,36 @@ export type SelectProps = Omit & { isLoading?: boolean; onChange?: (val: T) => any | Promise; ScrollData?: ReturnType['ScrollData']; + customOnOpen?: () => void; + customOnClose?: () => void; +}; + +export const menuItemStyles: MenuItemProps = { + borderRadius: 'sm', + py: 2, + display: 'flex', + alignItems: 'center', + _hover: { + backgroundColor: 'myGray.100' + }, + _notLast: { + mb: 1 + } }; const MySelect = ( { placeholder, value, + valueLabel, isSearch = false, width = '100%', list = [], onChange, isLoading = false, ScrollData, + customOnOpen, + customOnClose, ...props }: SelectProps, ref: ForwardedRef<{ @@ -72,21 +92,19 @@ const MySelect = ( const SelectedItemRef = useRef(null); const SearchInputRef = useRef(null); - const menuItemStyles: MenuItemProps = { - borderRadius: 'sm', - py: 2, - display: 'flex', - alignItems: 'center', - _hover: { - backgroundColor: 'myGray.100' - }, - _notLast: { - mb: 1 - } - }; - const { isOpen, onOpen, onClose } = useDisclosure(); + const { isOpen, onOpen: defaultOnOpen, onClose: defaultOnClose } = useDisclosure(); const selectItem = useMemo(() => list.find((item) => item.value === value), [list, value]); + const onOpen = () => { + defaultOnOpen(); + customOnOpen?.(); + }; + + const onClose = () => { + defaultOnClose(); + customOnClose?.(); + }; + const [search, setSearch] = useState(''); const filterList = useMemo(() => { if (!isSearch || !search) { @@ -105,6 +123,7 @@ const MySelect = ( } })); + // Auto scroll useEffect(() => { if (isOpen && MenuListRef.current && SelectedItemRef.current) { const menu = MenuListRef.current; @@ -117,7 +136,7 @@ const MySelect = ( } }, [isSearch, isOpen]); - const { runAsync: onclickChange, loading } = useRequest2((val: T) => onChange?.(val)); + const { runAsync: onClickChange, loading } = useRequest2((val: T) => onChange?.(val)); const ListRender = useMemo(() => { return ( @@ -138,7 +157,7 @@ const MySelect = ( })} onClick={() => { if (value !== item.value) { - onclickChange(item.value); + onClickChange(item.value); } }} whiteSpace={'pre-wrap'} @@ -161,7 +180,7 @@ const MySelect = ( ))} ); - }, [filterList, value]); + }, [filterList, onClickChange, value]); const isSelecting = loading || isLoading; @@ -200,36 +219,48 @@ const MySelect = ( : {})} {...props} > - - {isSelecting && } - {isSearch && isOpen ? ( - setSearch(e.target.value)} - placeholder={ - selectItem?.alias || - (typeof selectItem?.label === 'string' ? selectItem?.label : placeholder) - } - size={'sm'} - w={'100%'} - color={'myGray.700'} - onBlur={() => { - setTimeout(() => { - SearchInputRef?.current?.focus(); - }, 0); - }} - /> - ) : ( - <> - {selectItem?.icon && ( - - )} - {selectItem?.alias || selectItem?.label || placeholder} - - )} + + + {isSelecting && } + {valueLabel ? ( + <>{valueLabel} + ) : ( + <> + {isSearch && isOpen ? ( + setSearch(e.target.value)} + placeholder={ + (typeof selectItem?.alias === 'string' ? selectItem?.alias : '') || + (typeof selectItem?.label === 'string' ? selectItem?.label : placeholder) + } + size={'sm'} + w={'100%'} + color={'myGray.700'} + onBlur={() => { + setTimeout(() => { + SearchInputRef?.current?.focus(); + }, 0); + }} + /> + ) : ( + <> + {selectItem?.icon && ( + + )} + {selectItem?.alias || selectItem?.label || placeholder} + + )} + + )} + @@ -252,7 +283,7 @@ const MySelect = ( '0px 2px 4px rgba(161, 167, 179, 0.25), 0px 0px 1px rgba(121, 141, 159, 0.25);' } zIndex={99} - maxH={'40vh'} + maxH={'45vh'} overflowY={'auto'} > {ScrollData ? {ListRender} : ListRender} diff --git a/packages/web/hooks/useScrollPagination.tsx b/packages/web/hooks/useScrollPagination.tsx index 83447ebed..04b96b842 100644 --- a/packages/web/hooks/useScrollPagination.tsx +++ b/packages/web/hooks/useScrollPagination.tsx @@ -190,6 +190,7 @@ export function useScrollPagination< params = {}, EmptyTip, showErrorToast = true, + disalbed = false, ...props }: { scrollLoadType?: 'top' | 'bottom'; @@ -198,6 +199,7 @@ export function useScrollPagination< params?: Record; EmptyTip?: React.JSX.Element; showErrorToast?: boolean; + disalbed?: boolean; } & Parameters[1] ) { const { t } = useTranslation(); @@ -345,10 +347,10 @@ export function useScrollPagination< // Reload data useRequest2( async () => { + if (disalbed) return; loadData(true); }, { - manual: false, ...props } ); diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index 3991225c1..b00bc635e 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -1,4 +1,6 @@ { + "Click_to_delete_this_field": "Click to delete this field", + "Filed_is_deprecated": "This field is deprecated", "MCP_tools_list_is_empty": "MCP tool not resolved", "MCP_tools_parse_failed": "Failed to parse MCP address", "MCP_tools_url": "MCP Address", @@ -6,7 +8,6 @@ "MCP_tools_url_placeholder": "After filling in the MCP address, click Analysis", "Role_setting": "Permission", "Run": "Execute", - "team_tags_set": "Team tags", "Team_Tags": "Team tags", "ai_point_price": "Billing", "ai_settings": "AI Configuration", @@ -106,6 +107,7 @@ "no_mcp_tools_list": "No data yet, the MCP address needs to be parsed first", "node_not_intro": "This node is not introduced", "not_json_file": "Please select a JSON file", + "not_the_newest": "Not the latest", "oaste_curl_string": "Enter CURL code", "open_auto_execute": "Enable automatic execution", "open_vision_function_tip": "Models with icon switches have image recognition capabilities. \nAfter being turned on, the model will parse the pictures in the file link and automatically parse the pictures in the user's question (user question ≤ 500 words).", @@ -138,6 +140,7 @@ "stop_sign_placeholder": "Multiple serial numbers are separated by |, for example: aaa|stop", "stream_response": "Stream", "stream_response_tip": "Turning this switch off forces the model to use non-streaming mode and will not output content directly. \nIn the output of the AI ​​reply, the content output by this model can be obtained for secondary processing.", + "team_tags_set": "Team tags", "temperature": "Temperature", "temperature_tip": "Range 0~10. \nThe larger the value, the more divergent the model’s answer is; the smaller the value, the more rigorous the answer.", "template.hard_strict": "Strict Q&A template", diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index f406820bf..76ea7aeca 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -96,6 +96,7 @@ "add_new_param": "Add new param", "all_quotes": "All quotes", "all_result": "Full Results", + "app_not_version": "This application has not been published, please publish it first", "back": "Back", "base_config": "Basic Configuration", "bill_already_processed": "Order has been processed", diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index 485b5d39e..7b1fb99f9 100644 --- a/packages/web/i18n/zh-CN/app.json +++ b/packages/web/i18n/zh-CN/app.json @@ -1,4 +1,6 @@ { + "Click_to_delete_this_field": "点击删除该字段", + "Filed_is_deprecated": "该字段已弃用", "MCP_tools_debug": "调试", "MCP_tools_detail": "查看详情", "MCP_tools_list": "工具列表", @@ -10,7 +12,6 @@ "MCP_tools_url_placeholder": "填入 MCP 地址后,点击解析", "Role_setting": "权限设置", "Run": "运行", - "team_tags_set": "团队标签", "Team_Tags": "团队标签", "ai_point_price": "AI积分计费", "ai_settings": "AI 配置", @@ -110,6 +111,7 @@ "no_mcp_tools_list": "暂无数据,需先解析 MCP 地址", "node_not_intro": "这个节点没有介绍", "not_json_file": "请选择JSON文件", + "not_the_newest": "非最新版", "oaste_curl_string": "输入 CURL 代码", "open_auto_execute": "启用自动执行", "open_vision_function_tip": "有图示开关的模型即拥有图片识别能力。若开启,模型会解析文件链接里的图片,并自动解析用户问题中的图片(用户问题≤500字时生效)。", @@ -143,6 +145,7 @@ "stop_sign_placeholder": "多个序列号通过 | 隔开,例如:aaa|stop", "stream_response": "流输出", "stream_response_tip": "关闭该开关,可以强制模型使用非流模式,并且不会直接进行内容输出。可以在 AI 回复的输出中,获取本次模型输出的内容进行二次处理。", + "team_tags_set": "团队标签", "temperature": "温度", "temperature_tip": "范围 0~10。值越大,代表模型回答越发散;值越小,代表回答越严谨。", "template.hard_strict": "严格问答模板", diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index 75ebb9394..2bb374285 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -96,6 +96,7 @@ "add_new_param": "新增参数", "all_quotes": "全部引用", "all_result": "完整结果", + "app_not_version": " 该应用未发布过,请先发布应用", "back": "返回", "base_config": "基础配置", "bill_already_processed": "订单已处理", diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json index cf6125e4d..37f168035 100644 --- a/packages/web/i18n/zh-Hant/app.json +++ b/packages/web/i18n/zh-Hant/app.json @@ -1,4 +1,6 @@ { + "Click_to_delete_this_field": "點擊刪除該字段", + "Filed_is_deprecated": "該字段已棄用", "MCP_tools_list_is_empty": "未解析到 MCP 工具", "MCP_tools_parse_failed": "解析 MCP 地址失敗", "MCP_tools_url": "MCP 地址", @@ -6,7 +8,6 @@ "MCP_tools_url_placeholder": "填入 MCP 地址後,點擊解析", "Role_setting": "權限設定", "Run": "執行", - "team_tags_set": "團隊標籤", "Team_Tags": "團隊標籤", "ai_point_price": "AI 積分計費", "ai_settings": "AI 設定", @@ -106,6 +107,7 @@ "no_mcp_tools_list": "暫無數據,需先解析 MCP 地址", "node_not_intro": "這個節點沒有介紹", "not_json_file": "請選擇 JSON 檔案", + "not_the_newest": "非最新版", "oaste_curl_string": "輸入 CURL 代碼", "open_auto_execute": "啟用自動執行", "open_vision_function_tip": "有圖示開關的模型即擁有圖片辨識功能。若開啟,模型會解析檔案連結中的圖片,並自動解析使用者問題中的圖片(使用者問題 ≤ 500 字時生效)。", @@ -138,6 +140,7 @@ "stop_sign_placeholder": "多個序列號透過 | 隔開,例如:aaa|stop", "stream_response": "流輸出", "stream_response_tip": "關閉該開關​​,可以強制模型使用非流模式,並且不會直接進行內容輸出。\n可在 AI 回覆的輸出中,取得本次模型輸出的內容進行二次處理。", + "team_tags_set": "團隊標籤", "temperature": "溫度", "temperature_tip": "範圍 0~10。\n值越大,代表模型回答越發散;值越小,代表回答越嚴謹。", "template.hard_strict": "嚴格問答範本", diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index 55485c881..c9c803277 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -96,6 +96,7 @@ "add_new_param": "新增參數", "all_quotes": "全部引用", "all_result": "完整結果", + "app_not_version": "該應用未發布過,請先發布應用", "back": "返回", "base_config": "基本設定", "bill_already_processed": "訂單已處理", diff --git a/projects/app/src/pageComponents/app/detail/PublishHistoriesSlider.tsx b/projects/app/src/pageComponents/app/detail/PublishHistoriesSlider.tsx index 25fdb155c..1635619a9 100644 --- a/projects/app/src/pageComponents/app/detail/PublishHistoriesSlider.tsx +++ b/projects/app/src/pageComponents/app/detail/PublishHistoriesSlider.tsx @@ -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 diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/NodeTemplatesModal.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/NodeTemplatesModal.tsx index ca8b1ee1b..813585a34 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/NodeTemplatesModal.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/NodeTemplatesModal.tsx @@ -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, diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx index b9618f8a5..0fc27d8fe 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx @@ -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) => { > - - {hasNewVersion && ( - - - - )} - {!!nodeTemplate?.diagram && !hasNewVersion && ( + + {isAppNode && } + {!!nodeTemplate?.diagram && ( { {!!nodeTemplate?.diagram && node?.courseUrl && ( )} - {!!(node?.courseUrl || nodeTemplate?.userGuide) && !hasNewVersion && ( + {!!(node?.courseUrl || nodeTemplate?.userGuide) && ( { )} {!!node?.pluginData?.error && ( - + { fontWeight={'medium'} > - {t('app:app.modules.not_found')} + + {node?.pluginData?.error || t('app:app.modules.not_found')} + )} @@ -333,18 +272,14 @@ const NodeCard = (props: Props) => { ); }, [ - 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} - ); @@ -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 ( + + {node?.versionLabel} + {!node.isLatestVersion && ( + + {t('app:not_the_newest')} + + )} + + ); + }, [node.isLatestVersion, node?.versionLabel, t]); + + return ( + ( + + {props.children} + + )} + valueLabel={valueLabel} + /> + ); +}); diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/Label.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/Label.tsx index 3f52167a1..6af940ec9 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/Label.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/Label.tsx @@ -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) => { )} + {input.deprecated && ( + <> + + + { + onChangeNode({ + nodeId, + type: 'delInput', + key: input.key + }); + }} + > + + {t('app:Filed_is_deprecated')} + + + + )} + {/* Right Component */} - {RightComponent && ( + {!input.deprecated && RightComponent && ( <> {RightComponent} diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderOutput/Label.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderOutput/Label.tsx index d741686fa..c37d2d7df 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderOutput/Label.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderOutput/Label.tsx @@ -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 ( {description && } + + {output.deprecated && ( + <> + + + { + onChangeNode({ + nodeId, + type: 'delOutput', + key: output.key + }); + }} + > + + {t('app:Filed_is_deprecated')} + + + + )} {output.type === FlowNodeOutputTypeEnum.source && ( , _res: NextApiResponse ): Promise { - const { appId } = req.query; + const { appId, versionId } = req.query; const { source } = await splitCombinePluginId(appId); @@ -27,7 +27,7 @@ async function handler( await authApp({ req, authToken: true, appId, per: ReadPermissionVal }); } - return getChildAppPreviewNode({ id: appId }); + return getChildAppPreviewNode({ appId, versionId }); } export default NextAPI(handler); diff --git a/projects/app/src/pages/api/core/app/version/list.ts b/projects/app/src/pages/api/core/app/version/list.ts index 847724135..00df6b2df 100644 --- a/projects/app/src/pages/api/core/app/version/list.ts +++ b/projects/app/src/pages/api/core/app/version/list.ts @@ -11,6 +11,7 @@ import { addSourceMember } from '@fastgpt/service/support/user/utils'; export type versionListBody = PaginationProps<{ appId: string; + isPublish?: boolean; }>; export type versionListResponse = PaginationResponse; @@ -19,16 +20,19 @@ async function handler( req: ApiRequestProps, _res: NextApiResponse ): Promise { - const { appId } = req.body; + const { appId, isPublish } = req.body; const { offset, pageSize } = parsePaginationRequest(req); await authApp({ appId, req, per: WritePermissionVal, authToken: true }); + const match = { + appId, + isPublish + }; + const [result, total] = await Promise.all([ (async () => { - const versions = await MongoAppVersion.find({ - appId - }) + const versions = await MongoAppVersion.find(match) .sort({ time: -1 }) @@ -45,7 +49,7 @@ async function handler( })) ); })(), - MongoAppVersion.countDocuments({ appId }) + MongoAppVersion.countDocuments(match) ]); return { diff --git a/projects/app/src/service/core/app/utils.ts b/projects/app/src/service/core/app/utils.ts index 18c3aa227..a808ece26 100644 --- a/projects/app/src/service/core/app/utils.ts +++ b/projects/app/src/service/core/app/utils.ts @@ -146,7 +146,7 @@ export const checkNode = async ({ }: { node: StoreNodeItemType; ownerTmbId: string; -}) => { +}): Promise => { const pluginId = node.pluginId; if (!pluginId) return node; @@ -160,7 +160,7 @@ export const checkNode = async ({ }); } - const preview = await getChildAppPreviewNode({ id: pluginId }); + const preview = await getChildAppPreviewNode({ appId: pluginId }); return { ...node, pluginData: { @@ -175,7 +175,6 @@ export const checkNode = async ({ } catch (error: any) { return { ...node, - isError: true, pluginData: { error } as PluginDataType diff --git a/projects/app/src/web/core/app/api/plugin.ts b/projects/app/src/web/core/app/api/plugin.ts index 4f35abf22..396ca1326 100644 --- a/projects/app/src/web/core/app/api/plugin.ts +++ b/projects/app/src/web/core/app/api/plugin.ts @@ -7,7 +7,7 @@ import type { } from '@fastgpt/global/core/workflow/type/node'; import { getMyApps } from '../api'; import type { ListAppBody } from '@/pages/api/core/app/list'; -import { defaultNodeVersion, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants'; import type { GetPreviewNodeQuery } from '@/pages/api/core/app/plugin/getPreviewNode'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; @@ -47,7 +47,7 @@ export const getTeamPlugTemplates = (data?: ListAppBody) => name: app.name, intro: app.intro, showStatus: false, - version: app.pluginData?.nodeVersion || defaultNodeVersion, + version: app.pluginData?.nodeVersion, isTool: true, sourceMember: app.sourceMember })) diff --git a/projects/app/src/web/core/app/api/version.ts b/projects/app/src/web/core/app/api/version.ts index af1346ec3..81c5bbe98 100644 --- a/projects/app/src/web/core/app/api/version.ts +++ b/projects/app/src/web/core/app/api/version.ts @@ -15,7 +15,7 @@ export const getAppLatestVersion = (data: getLatestVersionQuery) => export const postPublishApp = (appId: string, data: PostPublishAppProps) => POST(`/core/app/version/publish?appId=${appId}`, data); -export const getWorkflowVersionList = (data: PaginationProps<{ appId: string }>) => +export const getAppVersionList = (data: PaginationProps<{ appId: string }>) => POST('/core/app/version/list', data); export const getAppVersionDetail = (versionId: string, appId: string) => diff --git a/projects/app/src/web/core/app/utils.ts b/projects/app/src/web/core/app/utils.ts index 63aa15824..ecef68ee7 100644 --- a/projects/app/src/web/core/app/utils.ts +++ b/projects/app/src/web/core/app/utils.ts @@ -7,7 +7,6 @@ import { import { type StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node.d'; import { chatHistoryValueDesc, - defaultNodeVersion, FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; @@ -402,7 +401,6 @@ export function form2AppWorkflow( y: 545 }, // 这里不需要固定版本,给一个不存在的版本,每次都会用最新版 - version: defaultNodeVersion, pluginData: tool.pluginData, inputs: tool.inputs.map((input) => { // Special key value diff --git a/projects/app/src/web/core/workflow/utils.ts b/projects/app/src/web/core/workflow/utils.ts index 103dd421c..6848736ec 100644 --- a/projects/app/src/web/core/workflow/utils.ts +++ b/projects/app/src/web/core/workflow/utils.ts @@ -9,8 +9,7 @@ import { EDGE_TYPE, FlowNodeInputTypeEnum, FlowNodeOutputTypeEnum, - FlowNodeTypeEnum, - defaultNodeVersion + FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { EmptyNode } from '@fastgpt/global/core/workflow/template/system/emptyNode'; import { type StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge'; @@ -101,10 +100,8 @@ export const storeNode2FlowNode = ({ ...template, ...storeNode, avatar: template.avatar ?? storeNode.avatar, - version: storeNode.version ?? template.version ?? defaultNodeVersion, - /* - Inputs and outputs, New fields are added, not reduced - */ + version: template.version || storeNode.version, + // template 中的输入必须都有 inputs: templateInputs .map((templateInput) => { const storeInput = @@ -113,10 +110,8 @@ export const storeNode2FlowNode = ({ return { ...storeInput, ...templateInput, - debugLabel: t(templateInput.debugLabel ?? (storeInput.debugLabel as any)), toolDescription: t(templateInput.toolDescription ?? (storeInput.toolDescription as any)), - selectedTypeIndex: storeInput.selectedTypeIndex ?? templateInput.selectedTypeIndex, value: storeInput.value, valueType: storeInput.valueType ?? templateInput.valueType, @@ -124,29 +119,29 @@ export const storeNode2FlowNode = ({ }; }) .concat( - /* Concat dynamic inputs */ + // 合并 store 中有,template 中没有的输入 storeNode.inputs .filter((item) => !templateInputs.find((input) => input.key === item.key)) .map((item) => { if (!dynamicInput) return item; + const templateInput = template.inputs.find((input) => input.key === item.key); return { ...item, - ...getInputComponentProps(dynamicInput) + ...getInputComponentProps(dynamicInput), + deprecated: templateInput?.deprecated }; }) ), outputs: templateOutputs .map((templateOutput) => { const storeOutput = - template.outputs.find((item) => item.key === templateOutput.key) || templateOutput; + storeNode.outputs.find((item) => item.key === templateOutput.key) || templateOutput; return { ...storeOutput, ...templateOutput, - description: t(templateOutput.description ?? (storeOutput.description as any)), - id: storeOutput.id ?? templateOutput.id, label: storeOutput.label ?? templateOutput.label, value: storeOutput.value ?? templateOutput.value, @@ -154,9 +149,15 @@ export const storeNode2FlowNode = ({ }; }) .concat( - storeNode.outputs.filter( - (item) => !templateOutputs.find((output) => output.key === item.key) - ) + storeNode.outputs + .filter((item) => !templateOutputs.find((output) => output.key === item.key)) + .map((item) => { + const templateOutput = template.outputs.find((output) => output.key === item.key); + return { + ...item, + deprecated: templateOutput?.deprecated + }; + }) ) }; diff --git a/projects/app/test/cases/web/workflow/store2flow.deprecated.test.ts b/projects/app/test/cases/web/workflow/store2flow.deprecated.test.ts new file mode 100644 index 000000000..a83dc648b --- /dev/null +++ b/projects/app/test/cases/web/workflow/store2flow.deprecated.test.ts @@ -0,0 +1,86 @@ +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; +import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; +import { storeNode2FlowNode } from '@/web/core/workflow/utils'; +import type { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; + +describe('storeNode2FlowNode with deprecated inputs/outputs', () => { + beforeEach(() => { + vi.mock('@fastgpt/global/core/workflow/template/constants', () => { + return { + moduleTemplatesFlat: [ + { + flowNodeType: 'userInput', + name: 'User Input', + avatar: '', + intro: '', + version: '1.0', + inputs: [ + { + key: 'deprecatedInput', + deprecated: true, + label: 'Deprecated Input', + renderTypeList: ['input'], + selectedTypeIndex: 0 + } + ], + outputs: [ + { + key: 'deprecatedOutput', + id: 'deprecatedId', + type: 'input', + deprecated: true, + label: 'Deprecated Output' + } + ] + } + ] + }; + }); + }); + + afterEach(() => { + vi.resetAllMocks(); + vi.resetModules(); + }); + + it('should handle deprecated inputs and outputs', () => { + const storeNode = { + nodeId: 'node1', + flowNodeType: 'userInput' as FlowNodeTypeEnum, + position: { x: 0, y: 0 }, + inputs: [ + { + key: 'deprecatedInput', + value: 'old value', + renderTypeList: ['input'], + label: 'Deprecated Input' + } + ], + outputs: [ + { + key: 'deprecatedOutput', + id: 'deprecatedId', + type: 'input', + label: 'Deprecated Output' + } + ], + name: 'Test Node', + version: '1.0' + }; + + const result = storeNode2FlowNode({ + item: storeNode as any, + t: ((key: any) => key) as any + }); + + const deprecatedInput = result.data.inputs.find((input) => input.key === 'deprecatedInput'); + expect(deprecatedInput).toBeDefined(); + expect(deprecatedInput?.deprecated).toBe(true); + + const deprecatedOutput = result.data.outputs.find( + (output) => output.key === 'deprecatedOutput' + ); + expect(deprecatedOutput).toBeDefined(); + expect(deprecatedOutput?.deprecated).toBe(true); + }); +}); diff --git a/projects/app/test/cases/web/workflow/store2flow.version.test.ts b/projects/app/test/cases/web/workflow/store2flow.version.test.ts new file mode 100644 index 000000000..f1bd27a46 --- /dev/null +++ b/projects/app/test/cases/web/workflow/store2flow.version.test.ts @@ -0,0 +1,113 @@ +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; +import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; +import { storeNode2FlowNode } from '@/web/core/workflow/utils'; +import type { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; + +describe('storeNode2FlowNode with version and avatar inheritance', () => { + beforeEach(() => { + vi.mock('@fastgpt/global/core/workflow/template/constants', () => { + return { + moduleTemplatesFlat: [ + { + flowNodeType: 'userInput', + name: 'User Input', + avatar: 'template-avatar.png', + intro: '', + version: '2.0', + inputs: [], + outputs: [] + } + ] + }; + }); + vi.mock('@fastgpt/global/core/workflow/node/constant', () => { + return { + FlowNodeTypeEnum: { userInput: 'userInput' }, + FlowNodeInputTypeEnum: { + addInputParam: 'addInputParam', + input: 'input', + reference: 'reference', + textarea: 'textarea', + numberInput: 'numberInput', + switch: 'switch', + select: 'select' + }, + FlowNodeOutputTypeEnum: { + dynamic: 'dynamic', + static: 'static', + source: 'source', + hidden: 'hidden' + }, + EDGE_TYPE: 'custom-edge', + chatHistoryValueDesc: 'chat history description', + datasetSelectValueDesc: 'dataset value description', + datasetQuoteValueDesc: 'dataset quote value description' + }; + }); + }); + + afterEach(() => { + vi.resetAllMocks(); + vi.resetModules(); + }); + + it('should handle version and avatar inheritance', () => { + // 测试场景1:storeNode没有version,使用template的version + const storeNode1 = { + nodeId: 'node1', + flowNodeType: 'userInput' as FlowNodeTypeEnum, + position: { x: 0, y: 0 }, + inputs: [], + outputs: [], + name: 'Test Node 1' + }; + + // 测试场景2:storeNode没有avatar,使用template的avatar + const storeNode2 = { + nodeId: 'node2', + flowNodeType: 'userInput' as FlowNodeTypeEnum, + position: { x: 0, y: 0 }, + inputs: [], + outputs: [], + name: 'Test Node 2', + version: '1.0' + }; + + // 测试场景3:storeNode和template都有avatar,使用template的avatar + const storeNode3 = { + nodeId: 'node3', + flowNodeType: 'userInput' as FlowNodeTypeEnum, + position: { x: 0, y: 0 }, + inputs: [], + outputs: [], + name: 'Test Node 3', + version: '3.0', + avatar: 'store-avatar.png' + }; + + const result1 = storeNode2FlowNode({ + item: storeNode1 as any, + t: ((key: any) => key) as any + }); + + const result2 = storeNode2FlowNode({ + item: storeNode2 as any, + t: ((key: any) => key) as any + }); + + const result3 = storeNode2FlowNode({ + item: storeNode3 as any, + t: ((key: any) => key) as any + }); + + // 验证版本继承关系 + expect(result1.data.version).toBe('2.0'); // 使用template的version + expect(result2.data.version).toBe('2.0'); // 使用storeNode的version + expect(result3.data.version).toBe('2.0'); // 使用storeNode的version + + // 验证avatar继承关系 + expect(result1.data.avatar).toBe('template-avatar.png'); // 使用template的avatar + expect(result2.data.avatar).toBe('template-avatar.png'); // 使用template的avatar + expect(result3.data.avatar).toBe('template-avatar.png'); // 根据源码,应该使用template的avatar + }); +}); diff --git a/projects/app/test/cases/web/workflow/utils.test.ts b/projects/app/test/cases/web/workflow/utils.test.ts new file mode 100644 index 000000000..4274363b6 --- /dev/null +++ b/projects/app/test/cases/web/workflow/utils.test.ts @@ -0,0 +1,339 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import type { + FlowNodeItemType, + FlowNodeTemplateType, + StoreNodeItemType +} from '@fastgpt/global/core/workflow/type/node'; +import type { Node, Edge } from 'reactflow'; +import { + FlowNodeTypeEnum, + FlowNodeInputTypeEnum, + FlowNodeOutputTypeEnum, + EDGE_TYPE +} from '@fastgpt/global/core/workflow/node/constant'; +import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; +import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; +import { + nodeTemplate2FlowNode, + storeNode2FlowNode, + filterWorkflowNodeOutputsByType, + checkWorkflowNodeAndConnection, + getLatestNodeTemplate +} from '@/web/core/workflow/utils'; +import type { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io'; + +describe('nodeTemplate2FlowNode', () => { + it('should convert template to flow node', () => { + const template: FlowNodeTemplateType = { + id: 'template1', + templateType: 'formInput', + name: 'Test Node', + flowNodeType: FlowNodeTypeEnum.formInput, + inputs: [], + outputs: [] + }; + + const result = nodeTemplate2FlowNode({ + template, + position: { x: 100, y: 100 }, + selected: true, + parentNodeId: 'parent1', + t: ((key: any) => key) as any + }); + + expect(result).toMatchObject({ + type: FlowNodeTypeEnum.formInput, + position: { x: 100, y: 100 }, + selected: true, + data: { + name: 'Test Node', + flowNodeType: FlowNodeTypeEnum.formInput, + parentNodeId: 'parent1' + } + }); + expect(result.id).toBeDefined(); + }); +}); + +describe('storeNode2FlowNode', () => { + it('should convert store node to flow node', () => { + const storeNode: StoreNodeItemType = { + nodeId: 'node1', + flowNodeType: FlowNodeTypeEnum.formInput, + position: { x: 100, y: 100 }, + inputs: [], + outputs: [], + name: 'Test Node', + version: '1.0' + }; + + const result = storeNode2FlowNode({ + item: storeNode, + selected: true, + t: ((key: any) => key) as any + }); + + expect(result).toMatchObject({ + id: 'node1', + type: FlowNodeTypeEnum.formInput, + position: { x: 100, y: 100 }, + selected: true + }); + }); + + it('should handle dynamic inputs and outputs', () => { + const storeNode: StoreNodeItemType = { + nodeId: 'node1', + flowNodeType: FlowNodeTypeEnum.formInput, + position: { x: 0, y: 0 }, + inputs: [ + { + key: 'dynamicInput', + label: 'Dynamic Input', + renderTypeList: [FlowNodeInputTypeEnum.addInputParam] + } + ], + outputs: [ + { + id: 'dynamicOutput', + key: 'dynamicOutput', + label: 'Dynamic Output', + type: FlowNodeOutputTypeEnum.dynamic + } + ], + name: 'Test Node', + version: '1.0' + }; + + const result = storeNode2FlowNode({ + item: storeNode, + t: ((key: any) => key) as any + }); + + expect(result.data.inputs).toHaveLength(3); + expect(result.data.outputs).toHaveLength(2); + }); + + // 这两个测试涉及到模拟冲突,请运行单独的测试文件: + // - utils.deprecated.test.ts: 测试 deprecated inputs/outputs + // - utils.version.test.ts: 测试 version 和 avatar inheritance +}); + +describe('filterWorkflowNodeOutputsByType', () => { + it('should filter outputs by type', () => { + const outputs: FlowNodeOutputItemType[] = [ + { + id: '1', + valueType: WorkflowIOValueTypeEnum.string, + key: '1', + label: '1', + type: FlowNodeOutputTypeEnum.static + }, + { + id: '2', + valueType: WorkflowIOValueTypeEnum.number, + key: '2', + label: '2', + type: FlowNodeOutputTypeEnum.static + }, + { + id: '3', + valueType: WorkflowIOValueTypeEnum.boolean, + key: '3', + label: '3', + type: FlowNodeOutputTypeEnum.static + } + ]; + + const result = filterWorkflowNodeOutputsByType(outputs, WorkflowIOValueTypeEnum.string); + + expect(result).toHaveLength(1); + expect(result[0].id).toBe('1'); + }); + + it('should return all outputs for any type', () => { + const outputs: FlowNodeOutputItemType[] = [ + { + id: '1', + valueType: WorkflowIOValueTypeEnum.string, + key: '1', + label: '1', + type: FlowNodeOutputTypeEnum.static + }, + { + id: '2', + valueType: WorkflowIOValueTypeEnum.number, + key: '2', + label: '2', + type: FlowNodeOutputTypeEnum.static + } + ]; + + const result = filterWorkflowNodeOutputsByType(outputs, WorkflowIOValueTypeEnum.any); + + expect(result).toHaveLength(2); + }); + + it('should handle array types correctly', () => { + const outputs: FlowNodeOutputItemType[] = [ + { + id: '1', + valueType: WorkflowIOValueTypeEnum.string, + key: '1', + label: '1', + type: FlowNodeOutputTypeEnum.static + }, + { + id: '2', + valueType: WorkflowIOValueTypeEnum.arrayString, + key: '2', + label: '2', + type: FlowNodeOutputTypeEnum.static + } + ]; + + const result = filterWorkflowNodeOutputsByType(outputs, WorkflowIOValueTypeEnum.arrayString); + expect(result).toHaveLength(2); + }); +}); + +describe('checkWorkflowNodeAndConnection', () => { + it('should validate nodes and connections', () => { + const nodes: Node[] = [ + { + id: 'node1', + type: FlowNodeTypeEnum.formInput, + data: { + nodeId: 'node1', + flowNodeType: FlowNodeTypeEnum.formInput, + inputs: [ + { + key: NodeInputKeyEnum.aiChatDatasetQuote, + required: true, + value: undefined, + renderTypeList: [FlowNodeInputTypeEnum.input] + } + ], + outputs: [] + }, + position: { x: 0, y: 0 } + } + ]; + + const edges: Edge[] = [ + { + id: 'edge1', + source: 'node1', + target: 'node2', + type: EDGE_TYPE + } + ]; + + const result = checkWorkflowNodeAndConnection({ nodes, edges }); + expect(result).toEqual(['node1']); + }); + + it('should handle empty nodes and edges', () => { + const result = checkWorkflowNodeAndConnection({ nodes: [], edges: [] }); + expect(result).toBeUndefined(); + }); +}); + +describe('getLatestNodeTemplate', () => { + it('should update node to latest template version', () => { + const node: FlowNodeItemType = { + id: 'node1', + nodeId: 'node1', + templateType: 'formInput', + flowNodeType: FlowNodeTypeEnum.formInput, + inputs: [ + { + key: 'input1', + value: 'test', + renderTypeList: [FlowNodeInputTypeEnum.input], + label: 'Input 1' + } + ], + outputs: [ + { + key: 'output1', + value: 'test', + type: FlowNodeOutputTypeEnum.static, + label: 'Output 1', + id: 'output1' + } + ], + name: 'Old Name', + intro: 'Old Intro' + }; + + const template: FlowNodeTemplateType = { + name: 'Template 1', + id: 'template1', + templateType: 'formInput', + flowNodeType: FlowNodeTypeEnum.formInput, + inputs: [ + { key: 'input1', renderTypeList: [FlowNodeInputTypeEnum.input], label: 'Input 1' }, + { key: 'input2', renderTypeList: [FlowNodeInputTypeEnum.input], label: 'Input 2' } + ], + outputs: [ + { id: 'output1', key: 'output1', type: FlowNodeOutputTypeEnum.static, label: 'Output 1' }, + { id: 'output2', key: 'output2', type: FlowNodeOutputTypeEnum.static, label: 'Output 2' } + ] + }; + + const result = getLatestNodeTemplate(node, template); + + expect(result.inputs).toHaveLength(2); + expect(result.outputs).toHaveLength(2); + expect(result.name).toBe('Old Name'); + }); + + it('should preserve existing values when updating template', () => { + const node: FlowNodeItemType = { + id: 'node1', + nodeId: 'node1', + templateType: 'formInput', + flowNodeType: FlowNodeTypeEnum.formInput, + inputs: [ + { + key: 'input1', + value: 'existingValue', + renderTypeList: [FlowNodeInputTypeEnum.input], + label: 'Input 1' + } + ], + outputs: [ + { + key: 'output1', + value: 'existingOutput', + type: FlowNodeOutputTypeEnum.static, + label: 'Output 1', + id: 'output1' + } + ], + name: 'Node Name', + intro: 'Node Intro' + }; + + const template: FlowNodeTemplateType = { + name: 'Template 1', + id: 'template1', + templateType: 'formInput', + flowNodeType: FlowNodeTypeEnum.formInput, + inputs: [ + { key: 'input1', renderTypeList: [FlowNodeInputTypeEnum.input], label: 'Input 1' }, + { key: 'input2', renderTypeList: [FlowNodeInputTypeEnum.input], label: 'Input 2' } + ], + outputs: [ + { id: 'output1', key: 'output1', type: FlowNodeOutputTypeEnum.static, label: 'Output 1' }, + { id: 'output2', key: 'output2', type: FlowNodeOutputTypeEnum.static, label: 'Output 2' } + ] + }; + + const result = getLatestNodeTemplate(node, template); + + expect(result.inputs[0].value).toBe('existingValue'); + expect(result.outputs[0].value).toBe('existingOutput'); + }); +}); diff --git a/scripts/openapi/openapi.ts b/scripts/openapi/openapi.ts index c07e76b1a..11ce3aae7 100644 --- a/scripts/openapi/openapi.ts +++ b/scripts/openapi/openapi.ts @@ -1,4 +1,4 @@ -import { ApiType } from './type'; +import type { ApiType } from './type'; type OpenAPIParameter = { name: string; diff --git a/test/cases/pages/api/support/mcp/server/toolList.test.ts b/test/cases/pages/api/support/mcp/server/toolList.test.ts index 404948586..2c06c7b18 100644 --- a/test/cases/pages/api/support/mcp/server/toolList.test.ts +++ b/test/cases/pages/api/support/mcp/server/toolList.test.ts @@ -20,7 +20,19 @@ vi.mock('@fastgpt/service/core/app/schema', () => ({ lean: vi.fn() }) }, - AppCollectionName: 'apps' + AppCollectionName: 'apps', + chatConfigType: { + welcomeText: String, + variables: Array, + questionGuide: Object, + ttsConfig: Object, + whisperConfig: Object, + scheduledTriggerConfig: Object, + chatInputGuide: Object, + fileSelectConfig: Object, + instruction: String, + autoExecute: Object + } })); vi.mock('@fastgpt/service/core/app/version/controller', () => ({ diff --git a/test/cases/web/core/app/utils.test.ts b/test/cases/web/core/app/utils.test.ts deleted file mode 100644 index 1093e864f..000000000 --- a/test/cases/web/core/app/utils.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - form2AppWorkflow, - filterSensitiveFormData, - getAppQGuideCustomURL -} from '@/web/core/app/utils'; -import { getDefaultAppForm } from '@fastgpt/global/core/app/utils'; -import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; -import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; -import type { AppSchema } from '@fastgpt/global/core/app/type'; - -describe('web/core/app/utils', () => { - const mockT = (text: string) => text; - - describe('form2AppWorkflow', () => { - it('should generate simple chat workflow', () => { - const form = getDefaultAppForm(); - const result = form2AppWorkflow(form, mockT); - - expect(result.nodes).toHaveLength(3); - expect(result.edges).toHaveLength(1); - expect(result.chatConfig).toBeDefined(); - }); - - it('should generate dataset workflow', () => { - const form = getDefaultAppForm(); - form.dataset.datasets = ['dataset1']; - const result = form2AppWorkflow(form, mockT); - - expect(result.nodes).toHaveLength(4); - expect(result.edges).toHaveLength(2); - }); - - it('should generate tools workflow', () => { - const form = getDefaultAppForm(); - form.selectedTools = [ - { - id: 'tool1', - name: 'Tool 1', - flowNodeType: FlowNodeTypeEnum.tools, - inputs: [], - outputs: [] - } - ]; - const result = form2AppWorkflow(form, mockT); - - expect(result.nodes.length).toBeGreaterThan(3); - expect(result.edges.length).toBeGreaterThan(1); - }); - }); - - describe('filterSensitiveFormData', () => { - it('should filter sensitive data', () => { - const form = getDefaultAppForm(); - form.dataset.datasets = ['sensitive']; - - const result = filterSensitiveFormData(form); - - expect(result.dataset).toEqual(getDefaultAppForm().dataset); - expect(result).not.toEqual(form); - }); - }); - - describe('getAppQGuideCustomURL', () => { - it('should get custom URL from app detail', () => { - const appDetail = { - modules: [ - { - flowNodeType: FlowNodeTypeEnum.systemConfig, - inputs: [ - { - key: NodeInputKeyEnum.chatInputGuide, - value: { - customUrl: 'https://example.com' - } - } - ] - } - ] - } as AppSchema; - - const result = getAppQGuideCustomURL(appDetail); - expect(result).toBe('https://example.com'); - }); - - it('should return empty string if no custom URL', () => { - const appDetail = { - modules: [ - { - flowNodeType: FlowNodeTypeEnum.systemConfig, - inputs: [ - { - key: NodeInputKeyEnum.chatInputGuide, - value: {} - } - ] - } - ] - } as AppSchema; - - const result = getAppQGuideCustomURL(appDetail); - expect(result).toBe(''); - }); - }); -}); diff --git a/test/cases/web/core/workflow/utils.test.ts b/test/cases/web/core/workflow/utils.test.ts deleted file mode 100644 index b454e8ae5..000000000 --- a/test/cases/web/core/workflow/utils.test.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { vi, describe, it, expect } from 'vitest'; -import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node'; -import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; -import type { Node, Edge } from 'reactflow'; -import { - FlowNodeTypeEnum, - FlowNodeInputTypeEnum, - FlowNodeOutputTypeEnum, - EDGE_TYPE -} from '@fastgpt/global/core/workflow/node/constant'; -import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; -import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; -import { - nodeTemplate2FlowNode, - storeNode2FlowNode, - storeEdgesRenderEdge, - computedNodeInputReference, - getRefData, - filterWorkflowNodeOutputsByType, - checkWorkflowNodeAndConnection, - getLatestNodeTemplate -} from '@/web/core/workflow/utils'; - -describe('workflow utils', () => { - describe('nodeTemplate2FlowNode', () => { - it('should convert template to flow node', () => { - const template: FlowNodeTemplateType = { - name: 'Test Node', - flowNodeType: FlowNodeTypeEnum.userInput, - inputs: [], - outputs: [] - }; - - const result = nodeTemplate2FlowNode({ - template, - position: { x: 100, y: 100 }, - selected: true, - parentNodeId: 'parent1', - t: (key) => key - }); - - expect(result).toMatchObject({ - type: FlowNodeTypeEnum.userInput, - position: { x: 100, y: 100 }, - selected: true, - data: { - name: 'Test Node', - flowNodeType: FlowNodeTypeEnum.userInput, - parentNodeId: 'parent1' - } - }); - expect(result.id).toBeDefined(); - }); - }); - - describe('storeNode2FlowNode', () => { - it('should convert store node to flow node', () => { - const storeNode: StoreNodeItemType = { - nodeId: 'node1', - flowNodeType: FlowNodeTypeEnum.userInput, - position: { x: 100, y: 100 }, - inputs: [], - outputs: [], - name: 'Test Node', - version: '1.0' - }; - - const result = storeNode2FlowNode({ - item: storeNode, - selected: true, - t: (key) => key - }); - - expect(result).toMatchObject({ - id: 'node1', - type: FlowNodeTypeEnum.userInput, - position: { x: 100, y: 100 }, - selected: true - }); - }); - - it('should handle dynamic inputs and outputs', () => { - const storeNode: StoreNodeItemType = { - nodeId: 'node1', - flowNodeType: FlowNodeTypeEnum.userInput, - position: { x: 0, y: 0 }, - inputs: [ - { - key: 'dynamicInput', - renderTypeList: [FlowNodeInputTypeEnum.addInputParam] - } - ], - outputs: [ - { - key: 'dynamicOutput', - type: FlowNodeOutputTypeEnum.dynamic - } - ], - name: 'Test Node', - version: '1.0' - }; - - const result = storeNode2FlowNode({ - item: storeNode, - t: (key) => key - }); - - expect(result.data.inputs).toHaveLength(1); - expect(result.data.outputs).toHaveLength(1); - }); - }); - - describe('filterWorkflowNodeOutputsByType', () => { - it('should filter outputs by type', () => { - const outputs = [ - { id: '1', valueType: WorkflowIOValueTypeEnum.string }, - { id: '2', valueType: WorkflowIOValueTypeEnum.number }, - { id: '3', valueType: WorkflowIOValueTypeEnum.boolean } - ]; - - const result = filterWorkflowNodeOutputsByType(outputs, WorkflowIOValueTypeEnum.string); - - expect(result).toHaveLength(1); - expect(result[0].id).toBe('1'); - }); - - it('should return all outputs for any type', () => { - const outputs = [ - { id: '1', valueType: WorkflowIOValueTypeEnum.string }, - { id: '2', valueType: WorkflowIOValueTypeEnum.number } - ]; - - const result = filterWorkflowNodeOutputsByType(outputs, WorkflowIOValueTypeEnum.any); - - expect(result).toHaveLength(2); - }); - - it('should handle array types correctly', () => { - const outputs = [ - { id: '1', valueType: WorkflowIOValueTypeEnum.string }, - { id: '2', valueType: WorkflowIOValueTypeEnum.arrayString } - ]; - - const result = filterWorkflowNodeOutputsByType(outputs, WorkflowIOValueTypeEnum.arrayString); - expect(result).toHaveLength(2); - }); - }); - - describe('checkWorkflowNodeAndConnection', () => { - it('should validate nodes and connections', () => { - const nodes: Node[] = [ - { - id: 'node1', - type: FlowNodeTypeEnum.userInput, - data: { - nodeId: 'node1', - flowNodeType: FlowNodeTypeEnum.userInput, - inputs: [ - { - key: NodeInputKeyEnum.userInput, - required: true, - value: undefined, - renderTypeList: [FlowNodeInputTypeEnum.input] - } - ], - outputs: [] - }, - position: { x: 0, y: 0 } - } - ]; - - const edges: Edge[] = [ - { - id: 'edge1', - source: 'node1', - target: 'node2', - type: EDGE_TYPE - } - ]; - - const result = checkWorkflowNodeAndConnection({ nodes, edges }); - expect(result).toEqual(['node1']); - }); - - it('should handle empty nodes and edges', () => { - const result = checkWorkflowNodeAndConnection({ nodes: [], edges: [] }); - expect(result).toBeUndefined(); - }); - }); - - describe('getLatestNodeTemplate', () => { - it('should update node to latest template version', () => { - const node = { - nodeId: 'node1', - flowNodeType: FlowNodeTypeEnum.userInput, - inputs: [{ key: 'input1', value: 'test' }], - outputs: [{ key: 'output1', value: 'test' }], - name: 'Old Name', - intro: 'Old Intro' - }; - - const template = { - flowNodeType: FlowNodeTypeEnum.userInput, - inputs: [{ key: 'input1' }, { key: 'input2' }], - outputs: [{ key: 'output1' }, { key: 'output2' }] - }; - - const result = getLatestNodeTemplate(node, template); - - expect(result.inputs).toHaveLength(2); - expect(result.outputs).toHaveLength(2); - expect(result.name).toBe('Old Name'); - }); - - it('should preserve existing values when updating template', () => { - const node = { - nodeId: 'node1', - flowNodeType: FlowNodeTypeEnum.userInput, - inputs: [{ key: 'input1', value: 'existingValue' }], - outputs: [{ key: 'output1', value: 'existingOutput' }], - name: 'Node Name', - intro: 'Node Intro' - }; - - const template = { - flowNodeType: FlowNodeTypeEnum.userInput, - inputs: [{ key: 'input1', value: 'newValue' }], - outputs: [{ key: 'output1', value: 'newOutput' }] - }; - - const result = getLatestNodeTemplate(node, template); - - expect(result.inputs[0].value).toBe('existingValue'); - expect(result.outputs[0].value).toBe('existingOutput'); - }); - }); -});