mirror of
https://github.com/labring/FastGPT.git
synced 2026-05-08 01:08:43 +08:00
feat: support multiple valueTypes for text input variables (#6801)
* feat: 文本输入框变量支持多 valueType * fix: 文本输入框变量非 string valueType 的渲染映射 - formRender/utils.ts: variableInputTypeToInputType 在 text input 场景下 按 valueType 派生,非 string 走 JSONEditor/textarea,让 Chat 表单与 调试面板能正确渲染 object/array/any 变量 - packages/global/core/workflow/utils.ts: appData2FlowNodeIO 的 renderTypeMap 对 text input + 非 string 使用 [JSONEditor, reference], 保证父工作流节点输入同步跟进 - 补 appData2FlowNodeIO 的 text input valueType 分支测试 * feat(workflow): 全局变量联动清理节点引用及变量更新节点必填校验 * fix(workflow): 文本输入变量 legacy 兼容与引用存活校验 * refactor(workflow): 抽出文本输入变量 valueType 兜底 helper * feat: adapt test for all node type * revert(workflow): 移除全局变量引用联动清理 * feat: remove any in text input * doc --------- Co-authored-by: archer <545436317@qq.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import InputTypeSelector from '@fastgpt/web/components/common/InputTypeSelector'
|
||||
import { getVariableInputTypeList } from '@fastgpt/web/components/common/InputTypeSelector/configs';
|
||||
import { addVariable } from '../VariableEdit';
|
||||
import { useValidateFieldName, useSubmitErrorHandler } from '../utils/formValidation';
|
||||
import { snapTextInputValueType } from './utils';
|
||||
|
||||
const VariableEditModal = ({
|
||||
onClose,
|
||||
@@ -38,7 +39,14 @@ const VariableEditModal = ({
|
||||
const type = watch('type');
|
||||
useEffect(() => {
|
||||
reset(variable);
|
||||
}, [variable, reset]);
|
||||
if (variable?.type === VariableInputEnum.input) {
|
||||
const snap = snapTextInputValueType(variable.valueType);
|
||||
if (snap.resetDefault) {
|
||||
setValue('valueType', snap.valueType);
|
||||
setValue('defaultValue', '');
|
||||
}
|
||||
}
|
||||
}, [variable, reset, setValue]);
|
||||
|
||||
const inputTypeList = useMemo(() => getVariableInputTypeList(), []);
|
||||
|
||||
@@ -67,6 +75,13 @@ const VariableEditModal = ({
|
||||
if (typeEnum === VariableInputEnum.file) {
|
||||
setValue('canLocalUpload', true);
|
||||
}
|
||||
if (typeEnum === VariableInputEnum.input) {
|
||||
const snap = snapTextInputValueType(value.valueType);
|
||||
if (snap.resetDefault) {
|
||||
setValue('valueType', snap.valueType);
|
||||
setValue('defaultValue', '');
|
||||
}
|
||||
}
|
||||
|
||||
setValue('type', typeEnum);
|
||||
},
|
||||
@@ -88,16 +103,17 @@ const VariableEditModal = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// For custom and internal types, user can select valueType manually, so don't override it
|
||||
// For other types, set valueType from defaultValueType
|
||||
// custom/internal/input 允许用户自选 valueType,其余强制用模板 defaultValueType
|
||||
if (
|
||||
![VariableInputEnum.custom, VariableInputEnum.internal, VariableInputEnum].includes(
|
||||
![VariableInputEnum.custom, VariableInputEnum.internal, VariableInputEnum.input].includes(
|
||||
data.type
|
||||
)
|
||||
) {
|
||||
data.valueType = inputTypeList
|
||||
.flat()
|
||||
.find((item) => item.value === data.type)?.defaultValueType;
|
||||
} else if (data.type === VariableInputEnum.input) {
|
||||
data.valueType = snapTextInputValueType(data.valueType).valueType;
|
||||
}
|
||||
|
||||
// Special types set required = false
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
WorkflowIOValueTypeEnum,
|
||||
textInputVariableValueTypes
|
||||
} from '@fastgpt/global/core/workflow/constants';
|
||||
|
||||
// 对 text input 变量的 valueType 做 legacy 兜底:
|
||||
// - undefined: 视为隐式 string,不清 defaultValue
|
||||
// - 非法值: snap 回 string 并要求清空 defaultValue
|
||||
// - 合法值: 原样保留
|
||||
export const snapTextInputValueType = (
|
||||
valueType: WorkflowIOValueTypeEnum | undefined
|
||||
): { valueType: WorkflowIOValueTypeEnum; resetDefault: boolean } => {
|
||||
if (valueType === undefined) {
|
||||
return { valueType: WorkflowIOValueTypeEnum.string, resetDefault: false };
|
||||
}
|
||||
if (!textInputVariableValueTypes.includes(valueType)) {
|
||||
return { valueType: WorkflowIOValueTypeEnum.string, resetDefault: true };
|
||||
}
|
||||
return { valueType, resetDefault: false };
|
||||
};
|
||||
@@ -10,7 +10,9 @@ export const variableInputTypeToInputType = (
|
||||
inputType: VariableInputEnum,
|
||||
valueType?: WorkflowIOValueTypeEnum
|
||||
) => {
|
||||
if (inputType === VariableInputEnum.input) return InputTypeEnum.input;
|
||||
if (inputType === VariableInputEnum.input) {
|
||||
return valueType ? valueTypeToInputType(valueType) : InputTypeEnum.input;
|
||||
}
|
||||
if (inputType === VariableInputEnum.textarea) return InputTypeEnum.textarea;
|
||||
if (inputType === VariableInputEnum.numberInput) return InputTypeEnum.numberInput;
|
||||
if (inputType === VariableInputEnum.select) return InputTypeEnum.select;
|
||||
|
||||
+1
-1
@@ -193,7 +193,7 @@ const VariableInputForm = ({
|
||||
isUnChange={isUnChange}
|
||||
key={item.key}
|
||||
description={item.description}
|
||||
inputType={variableInputTypeToInputType(item.type)}
|
||||
inputType={variableInputTypeToInputType(item.type, item.valueType)}
|
||||
bg={'myGray.50'}
|
||||
form={variablesForm}
|
||||
fieldName={`variables.${item.key}`}
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ const ChatHomeVariablesForm = ({ chatForm }: Props) => {
|
||||
key={item.key}
|
||||
fieldName={`variables.${item.key}`}
|
||||
description={item.description}
|
||||
inputType={variableInputTypeToInputType(item.type)}
|
||||
inputType={variableInputTypeToInputType(item.type, item.valueType)}
|
||||
form={variablesForm}
|
||||
bg={'myGray.50'}
|
||||
/>
|
||||
|
||||
@@ -105,7 +105,7 @@ const VariablePopover = ({ chatType }: { chatType: ChatTypeEnum }) => {
|
||||
{...item}
|
||||
key={item.key}
|
||||
description={item.description}
|
||||
inputType={variableInputTypeToInputType(item.type)}
|
||||
inputType={variableInputTypeToInputType(item.type, item.valueType)}
|
||||
form={variablesForm}
|
||||
fieldName={`variables.${item.key}`}
|
||||
bg={'myGray.50'}
|
||||
@@ -138,7 +138,7 @@ const VariablePopover = ({ chatType }: { chatType: ChatTypeEnum }) => {
|
||||
{...item}
|
||||
key={item.key}
|
||||
description={item.description}
|
||||
inputType={variableInputTypeToInputType(item.type)}
|
||||
inputType={variableInputTypeToInputType(item.type, item.valueType)}
|
||||
form={variablesForm}
|
||||
fieldName={`variables.${item.key}`}
|
||||
bg={'myGray.50'}
|
||||
@@ -157,7 +157,7 @@ const VariablePopover = ({ chatType }: { chatType: ChatTypeEnum }) => {
|
||||
{...item}
|
||||
key={item.key}
|
||||
description={item.description}
|
||||
inputType={variableInputTypeToInputType(item.type)}
|
||||
inputType={variableInputTypeToInputType(item.type, item.valueType)}
|
||||
form={variablesForm}
|
||||
fieldName={`variables.${item.key}`}
|
||||
bg={'myGray.50'}
|
||||
|
||||
+3
-3
@@ -285,7 +285,7 @@ export const useDebug = () => {
|
||||
label={item.label}
|
||||
required={item.required}
|
||||
description={t(item.description)}
|
||||
inputType={variableInputTypeToInputType(item.type)}
|
||||
inputType={variableInputTypeToInputType(item.type, item.valueType)}
|
||||
form={variablesForm}
|
||||
fieldName={`variables.${item.key}`}
|
||||
bg={'myGray.50'}
|
||||
@@ -298,7 +298,7 @@ export const useDebug = () => {
|
||||
label={item.label}
|
||||
required={item.required}
|
||||
description={t(item.description)}
|
||||
inputType={variableInputTypeToInputType(item.type)}
|
||||
inputType={variableInputTypeToInputType(item.type, item.valueType)}
|
||||
form={variablesForm}
|
||||
fieldName={`variables.${item.key}`}
|
||||
bg={'myGray.50'}
|
||||
@@ -311,7 +311,7 @@ export const useDebug = () => {
|
||||
label={item.label}
|
||||
required={item.required}
|
||||
description={item.description}
|
||||
inputType={variableInputTypeToInputType(item.type)}
|
||||
inputType={variableInputTypeToInputType(item.type, item.valueType)}
|
||||
form={variablesForm}
|
||||
fieldName={`variables.${item.key}`}
|
||||
bg={'myGray.50'}
|
||||
|
||||
+27
-28
@@ -14,7 +14,8 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
VariableInputEnum,
|
||||
WorkflowIOValueTypeEnum
|
||||
WorkflowIOValueTypeEnum,
|
||||
textInputVariableValueTypes
|
||||
} from '@fastgpt/global/core/workflow/constants';
|
||||
import {
|
||||
FlowNodeInputTypeEnum,
|
||||
@@ -198,12 +199,21 @@ const InputTypeConfig = ({
|
||||
value: item.value
|
||||
}));
|
||||
|
||||
const showValueTypeSelect =
|
||||
inputType === FlowNodeInputTypeEnum.reference ||
|
||||
const isVariableTextInput = type === 'variable' && inputType === VariableInputEnum.input;
|
||||
|
||||
const isDynamicValueTypeInput =
|
||||
inputType === FlowNodeInputTypeEnum.customVariable ||
|
||||
inputType === FlowNodeInputTypeEnum.hidden ||
|
||||
inputType === VariableInputEnum.custom ||
|
||||
inputType === VariableInputEnum.internal;
|
||||
inputType === VariableInputEnum.internal ||
|
||||
isVariableTextInput;
|
||||
|
||||
const showValueTypeSelect =
|
||||
inputType === FlowNodeInputTypeEnum.reference || isDynamicValueTypeInput;
|
||||
|
||||
const valueTypeOptionList = isVariableTextInput
|
||||
? valueTypeSelectList.filter((item) => textInputVariableValueTypes.includes(item.value))
|
||||
: valueTypeSelectList.filter((item) => item.value !== WorkflowIOValueTypeEnum.arrayAny);
|
||||
|
||||
const showRequired = useMemo(() => {
|
||||
const list = [
|
||||
@@ -407,12 +417,13 @@ const InputTypeConfig = ({
|
||||
{showValueTypeSelect ? (
|
||||
<Box flex={1}>
|
||||
<MySelect<WorkflowIOValueTypeEnum>
|
||||
list={valueTypeSelectList.filter(
|
||||
(item) => item.value !== WorkflowIOValueTypeEnum.arrayAny
|
||||
)}
|
||||
list={valueTypeOptionList}
|
||||
value={valueType}
|
||||
onChange={(e) => {
|
||||
setValue('valueType', e);
|
||||
if (isVariableTextInput) {
|
||||
setValue('defaultValue', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
@@ -557,11 +568,7 @@ const InputTypeConfig = ({
|
||||
</FormLabel>
|
||||
<Flex flex={1} h={10}>
|
||||
{(inputType === FlowNodeInputTypeEnum.numberInput ||
|
||||
((inputType === VariableInputEnum.custom ||
|
||||
inputType === VariableInputEnum.internal ||
|
||||
inputType === FlowNodeInputTypeEnum.customVariable ||
|
||||
inputType === FlowNodeInputTypeEnum.hidden) &&
|
||||
valueType === WorkflowIOValueTypeEnum.number)) && (
|
||||
(isDynamicValueTypeInput && valueType === WorkflowIOValueTypeEnum.number)) && (
|
||||
<MyNumberInput
|
||||
value={defaultValue}
|
||||
min={min ? min : undefined}
|
||||
@@ -572,12 +579,10 @@ const InputTypeConfig = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(inputType === FlowNodeInputTypeEnum.input ||
|
||||
((inputType === VariableInputEnum.custom ||
|
||||
inputType === VariableInputEnum.internal ||
|
||||
inputType === FlowNodeInputTypeEnum.customVariable ||
|
||||
inputType === FlowNodeInputTypeEnum.hidden) &&
|
||||
valueType === WorkflowIOValueTypeEnum.string)) && (
|
||||
{((inputType === FlowNodeInputTypeEnum.input && !isVariableTextInput) ||
|
||||
(isDynamicValueTypeInput &&
|
||||
(valueType === WorkflowIOValueTypeEnum.string ||
|
||||
valueType === WorkflowIOValueTypeEnum.any))) && (
|
||||
<MyTextarea
|
||||
value={defaultValue}
|
||||
onChange={(e) => setValue('defaultValue', e.target.value)}
|
||||
@@ -589,14 +594,12 @@ const InputTypeConfig = ({
|
||||
/>
|
||||
)}
|
||||
{(inputType === FlowNodeInputTypeEnum.JSONEditor ||
|
||||
((inputType === VariableInputEnum.custom ||
|
||||
inputType === VariableInputEnum.internal ||
|
||||
inputType === FlowNodeInputTypeEnum.customVariable ||
|
||||
inputType === FlowNodeInputTypeEnum.hidden) &&
|
||||
(isDynamicValueTypeInput &&
|
||||
![
|
||||
WorkflowIOValueTypeEnum.number,
|
||||
WorkflowIOValueTypeEnum.string,
|
||||
WorkflowIOValueTypeEnum.boolean
|
||||
WorkflowIOValueTypeEnum.boolean,
|
||||
WorkflowIOValueTypeEnum.any
|
||||
].includes(valueType))) && (
|
||||
<JsonEditor
|
||||
bg={'myGray.50'}
|
||||
@@ -609,11 +612,7 @@ const InputTypeConfig = ({
|
||||
/>
|
||||
)}
|
||||
{(inputType === FlowNodeInputTypeEnum.switch ||
|
||||
((inputType === VariableInputEnum.custom ||
|
||||
inputType === VariableInputEnum.internal ||
|
||||
inputType === FlowNodeInputTypeEnum.customVariable ||
|
||||
inputType === FlowNodeInputTypeEnum.hidden) &&
|
||||
valueType === WorkflowIOValueTypeEnum.boolean)) && (
|
||||
(isDynamicValueTypeInput && valueType === WorkflowIOValueTypeEnum.boolean)) && (
|
||||
<Flex h={10} alignItems={'center'}>
|
||||
<Switch {...register('defaultValue')} />
|
||||
</Flex>
|
||||
|
||||
@@ -19,7 +19,9 @@ import { type EditorVariablePickerType } from '@fastgpt/web/components/common/Te
|
||||
import {
|
||||
formatEditorVariablePickerIcon,
|
||||
getAppChatConfig,
|
||||
getHandleId
|
||||
getHandleId,
|
||||
isValidReferenceValue,
|
||||
isValidReferenceValueFormat
|
||||
} from '@fastgpt/global/core/workflow/utils';
|
||||
import { type TFunction } from 'next-i18next';
|
||||
import {
|
||||
@@ -30,6 +32,7 @@ import {
|
||||
import { type IfElseListItemType } from '@fastgpt/global/core/workflow/template/system/ifElse/type';
|
||||
import { LoopRunModeEnum } from '@fastgpt/global/core/workflow/template/system/loopRun/loopRun';
|
||||
import { VariableConditionEnum } from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
|
||||
import { type TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type';
|
||||
import { type AppChatConfigType } from '@fastgpt/global/core/app/type';
|
||||
import { cloneDeep, isEqual } from 'lodash';
|
||||
import { workflowSystemVariables } from '../app/utils';
|
||||
@@ -545,6 +548,53 @@ export const checkWorkflowNodeAndConnection = ({
|
||||
return [data.nodeId];
|
||||
}
|
||||
}
|
||||
if (data.flowNodeType === FlowNodeTypeEnum.variableUpdate) {
|
||||
const updateList: TUpdateListItem[] = inputs.find(
|
||||
(input) => input.key === NodeInputKeyEnum.updateList
|
||||
)?.value;
|
||||
const nodeIds = nodes.map((n) => n.data.nodeId);
|
||||
const isLiveReference = (value: ReferenceItemValueType | undefined) => {
|
||||
if (!isValidReferenceValueFormat(value)) return false;
|
||||
const [refNodeId, refOutputId] = value;
|
||||
if (!refNodeId || !refOutputId) return false;
|
||||
if (refNodeId === VARIABLE_NODE_ID) return true;
|
||||
return !!nodes
|
||||
.find((node) => node.data.nodeId === refNodeId)
|
||||
?.data.outputs.find((output) => output.id === refOutputId);
|
||||
};
|
||||
if (
|
||||
!updateList ||
|
||||
updateList.length === 0 ||
|
||||
updateList.some((item) => {
|
||||
if (!isValidReferenceValue(item.variable, nodeIds) || !isLiveReference(item.variable))
|
||||
return true;
|
||||
|
||||
const isArrayVar =
|
||||
typeof item.valueType === 'string' && item.valueType.startsWith('array');
|
||||
|
||||
if (item.renderType === FlowNodeInputTypeEnum.reference) {
|
||||
if (isArrayVar) {
|
||||
return (
|
||||
!Array.isArray(item.value) ||
|
||||
item.value.length === 0 ||
|
||||
(item.value as ReferenceItemValueType[]).some((v) => !isLiveReference(v))
|
||||
);
|
||||
}
|
||||
return !isLiveReference(item.value as ReferenceItemValueType);
|
||||
}
|
||||
|
||||
// input mode: clear 不需要 value;boolean 由 booleanMode 决定
|
||||
if (isArrayVar && item.arrayMode === 'clear') return false;
|
||||
if (item.valueType === WorkflowIOValueTypeEnum.boolean) return false;
|
||||
const inputVal = item.value?.[1];
|
||||
return inputVal === undefined || inputVal === null || inputVal === '';
|
||||
})
|
||||
) {
|
||||
return [data.nodeId];
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
inputs.some((input) => {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { snapTextInputValueType } from '@/components/core/app/VariableEditModal/utils';
|
||||
|
||||
describe('snapTextInputValueType', () => {
|
||||
it('undefined → string,不清空 defaultValue(legacy 隐式 string)', () => {
|
||||
expect(snapTextInputValueType(undefined)).toEqual({
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
resetDefault: false
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
WorkflowIOValueTypeEnum.string,
|
||||
WorkflowIOValueTypeEnum.object,
|
||||
WorkflowIOValueTypeEnum.arrayString,
|
||||
WorkflowIOValueTypeEnum.arrayNumber,
|
||||
WorkflowIOValueTypeEnum.arrayBoolean,
|
||||
WorkflowIOValueTypeEnum.arrayObject
|
||||
])('合法 valueType %s 原样返回,不清 defaultValue', (valueType) => {
|
||||
expect(snapTextInputValueType(valueType)).toEqual({
|
||||
valueType,
|
||||
resetDefault: false
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
WorkflowIOValueTypeEnum.number,
|
||||
WorkflowIOValueTypeEnum.boolean,
|
||||
WorkflowIOValueTypeEnum.any,
|
||||
WorkflowIOValueTypeEnum.arrayAny,
|
||||
WorkflowIOValueTypeEnum.chatHistory,
|
||||
WorkflowIOValueTypeEnum.datasetQuote
|
||||
])('非法 valueType %s snap 回 string,并要求清 defaultValue', (valueType) => {
|
||||
expect(snapTextInputValueType(valueType)).toEqual({
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
resetDefault: true
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
VariableInputEnum,
|
||||
WorkflowIOValueTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/constants';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import {
|
||||
variableInputTypeToInputType,
|
||||
valueTypeToInputType,
|
||||
nodeInputTypeToInputType,
|
||||
secretInputTypeToInputType
|
||||
} from '@/components/core/app/formRender/utils';
|
||||
import { InputTypeEnum } from '@/components/core/app/formRender/constant';
|
||||
|
||||
describe('variableInputTypeToInputType', () => {
|
||||
it('input + string → input', () => {
|
||||
expect(
|
||||
variableInputTypeToInputType(VariableInputEnum.input, WorkflowIOValueTypeEnum.string)
|
||||
).toBe(InputTypeEnum.input);
|
||||
});
|
||||
|
||||
it('input + object → JSONEditor', () => {
|
||||
expect(
|
||||
variableInputTypeToInputType(VariableInputEnum.input, WorkflowIOValueTypeEnum.object)
|
||||
).toBe(InputTypeEnum.JSONEditor);
|
||||
});
|
||||
|
||||
it('input + arrayString → JSONEditor', () => {
|
||||
expect(
|
||||
variableInputTypeToInputType(VariableInputEnum.input, WorkflowIOValueTypeEnum.arrayString)
|
||||
).toBe(InputTypeEnum.JSONEditor);
|
||||
});
|
||||
|
||||
it('input + any → textarea(与 chat 侧保持)', () => {
|
||||
expect(variableInputTypeToInputType(VariableInputEnum.input, WorkflowIOValueTypeEnum.any)).toBe(
|
||||
InputTypeEnum.textarea
|
||||
);
|
||||
});
|
||||
|
||||
it('input + undefined(legacy)→ input', () => {
|
||||
expect(variableInputTypeToInputType(VariableInputEnum.input)).toBe(InputTypeEnum.input);
|
||||
});
|
||||
|
||||
it('numberInput/switch 等固定映射不受 valueType 影响', () => {
|
||||
expect(
|
||||
variableInputTypeToInputType(VariableInputEnum.numberInput, WorkflowIOValueTypeEnum.string)
|
||||
).toBe(InputTypeEnum.numberInput);
|
||||
expect(variableInputTypeToInputType(VariableInputEnum.switch)).toBe(InputTypeEnum.switch);
|
||||
});
|
||||
|
||||
it('custom/internal 根据 valueType 决定', () => {
|
||||
expect(
|
||||
variableInputTypeToInputType(VariableInputEnum.custom, WorkflowIOValueTypeEnum.object)
|
||||
).toBe(InputTypeEnum.JSONEditor);
|
||||
expect(
|
||||
variableInputTypeToInputType(VariableInputEnum.internal, WorkflowIOValueTypeEnum.number)
|
||||
).toBe(InputTypeEnum.numberInput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('valueTypeToInputType', () => {
|
||||
it('string/number/boolean → input/numberInput/switch', () => {
|
||||
expect(valueTypeToInputType(WorkflowIOValueTypeEnum.string)).toBe(InputTypeEnum.input);
|
||||
expect(valueTypeToInputType(WorkflowIOValueTypeEnum.number)).toBe(InputTypeEnum.numberInput);
|
||||
expect(valueTypeToInputType(WorkflowIOValueTypeEnum.boolean)).toBe(InputTypeEnum.switch);
|
||||
});
|
||||
|
||||
it('object/array* → JSONEditor', () => {
|
||||
expect(valueTypeToInputType(WorkflowIOValueTypeEnum.object)).toBe(InputTypeEnum.JSONEditor);
|
||||
expect(valueTypeToInputType(WorkflowIOValueTypeEnum.arrayString)).toBe(
|
||||
InputTypeEnum.JSONEditor
|
||||
);
|
||||
expect(valueTypeToInputType(WorkflowIOValueTypeEnum.arrayObject)).toBe(
|
||||
InputTypeEnum.JSONEditor
|
||||
);
|
||||
});
|
||||
|
||||
it('any/undefined → textarea', () => {
|
||||
expect(valueTypeToInputType(WorkflowIOValueTypeEnum.any)).toBe(InputTypeEnum.textarea);
|
||||
expect(valueTypeToInputType(undefined)).toBe(InputTypeEnum.textarea);
|
||||
});
|
||||
|
||||
it('chatHistory/datasetQuote/dynamic/selectDataset/selectApp → JSONEditor', () => {
|
||||
expect(valueTypeToInputType(WorkflowIOValueTypeEnum.chatHistory)).toBe(
|
||||
InputTypeEnum.JSONEditor
|
||||
);
|
||||
expect(valueTypeToInputType(WorkflowIOValueTypeEnum.datasetQuote)).toBe(
|
||||
InputTypeEnum.JSONEditor
|
||||
);
|
||||
expect(valueTypeToInputType(WorkflowIOValueTypeEnum.dynamic)).toBe(InputTypeEnum.JSONEditor);
|
||||
expect(valueTypeToInputType(WorkflowIOValueTypeEnum.selectDataset)).toBe(
|
||||
InputTypeEnum.JSONEditor
|
||||
);
|
||||
expect(valueTypeToInputType(WorkflowIOValueTypeEnum.selectApp)).toBe(InputTypeEnum.JSONEditor);
|
||||
expect(valueTypeToInputType(WorkflowIOValueTypeEnum.arrayAny)).toBe(InputTypeEnum.JSONEditor);
|
||||
expect(valueTypeToInputType(WorkflowIOValueTypeEnum.arrayNumber)).toBe(
|
||||
InputTypeEnum.JSONEditor
|
||||
);
|
||||
expect(valueTypeToInputType(WorkflowIOValueTypeEnum.arrayBoolean)).toBe(
|
||||
InputTypeEnum.JSONEditor
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nodeInputTypeToInputType', () => {
|
||||
it('跳过 reference,取到真正的控件类型', () => {
|
||||
expect(
|
||||
nodeInputTypeToInputType([FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.input])
|
||||
).toBe(InputTypeEnum.input);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[FlowNodeInputTypeEnum.input, InputTypeEnum.input],
|
||||
[FlowNodeInputTypeEnum.textarea, InputTypeEnum.textarea],
|
||||
[FlowNodeInputTypeEnum.password, InputTypeEnum.password],
|
||||
[FlowNodeInputTypeEnum.numberInput, InputTypeEnum.numberInput],
|
||||
[FlowNodeInputTypeEnum.switch, InputTypeEnum.switch],
|
||||
[FlowNodeInputTypeEnum.select, InputTypeEnum.select],
|
||||
[FlowNodeInputTypeEnum.multipleSelect, InputTypeEnum.multipleSelect],
|
||||
[FlowNodeInputTypeEnum.JSONEditor, InputTypeEnum.JSONEditor],
|
||||
[FlowNodeInputTypeEnum.selectLLMModel, InputTypeEnum.selectLLMModel],
|
||||
[FlowNodeInputTypeEnum.fileSelect, InputTypeEnum.fileSelect],
|
||||
[FlowNodeInputTypeEnum.timePointSelect, InputTypeEnum.timePointSelect],
|
||||
[FlowNodeInputTypeEnum.timeRangeSelect, InputTypeEnum.timeRangeSelect]
|
||||
])('%s → %s', (input, expected) => {
|
||||
expect(nodeInputTypeToInputType([input])).toBe(expected);
|
||||
});
|
||||
|
||||
it('空数组或未识别类型 → textarea', () => {
|
||||
expect(nodeInputTypeToInputType([])).toBe(InputTypeEnum.textarea);
|
||||
expect(nodeInputTypeToInputType()).toBe(InputTypeEnum.textarea);
|
||||
expect(nodeInputTypeToInputType([FlowNodeInputTypeEnum.reference])).toBe(
|
||||
InputTypeEnum.textarea
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('secretInputTypeToInputType', () => {
|
||||
it.each([
|
||||
['input', InputTypeEnum.input],
|
||||
['numberInput', InputTypeEnum.numberInput],
|
||||
['switch', InputTypeEnum.switch],
|
||||
['select', InputTypeEnum.select]
|
||||
] as const)('%s → %s', (input, expected) => {
|
||||
expect(secretInputTypeToInputType(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('未识别 → textarea', () => {
|
||||
expect(secretInputTypeToInputType('unknown' as any)).toBe(InputTypeEnum.textarea);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type {
|
||||
FlowNodeItemType,
|
||||
FlowNodeTemplateType,
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
checkWorkflowNodeAndConnection
|
||||
} from '@/web/core/workflow/utils';
|
||||
import type { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
import { VARIABLE_NODE_ID } from '@fastgpt/global/core/workflow/constants';
|
||||
|
||||
describe('nodeTemplate2FlowNode', () => {
|
||||
it('should convert template to flow node', () => {
|
||||
@@ -394,4 +395,163 @@ describe('checkWorkflowNodeAndConnection', () => {
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
describe('variableUpdate node', () => {
|
||||
const makeVarUpdateNode = (updateList: any[]): Node<FlowNodeItemType> =>
|
||||
({
|
||||
id: 'u1',
|
||||
type: FlowNodeTypeEnum.variableUpdate,
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
nodeId: 'u1',
|
||||
flowNodeType: FlowNodeTypeEnum.variableUpdate,
|
||||
name: 'update',
|
||||
avatar: '',
|
||||
inputs: [
|
||||
{
|
||||
key: NodeInputKeyEnum.updateList,
|
||||
valueType: WorkflowIOValueTypeEnum.any,
|
||||
renderTypeList: [FlowNodeInputTypeEnum.hidden],
|
||||
value: updateList
|
||||
}
|
||||
],
|
||||
outputs: [],
|
||||
version: '1',
|
||||
intro: ''
|
||||
} as any
|
||||
}) as any;
|
||||
|
||||
const startNode: Node<FlowNodeItemType> = {
|
||||
id: 's1',
|
||||
type: FlowNodeTypeEnum.workflowStart,
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
nodeId: 's1',
|
||||
flowNodeType: FlowNodeTypeEnum.workflowStart,
|
||||
name: 'start',
|
||||
avatar: '',
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
version: '1',
|
||||
intro: ''
|
||||
} as any
|
||||
};
|
||||
|
||||
const connectedEdges: Edge[] = [{ id: 'e1', source: 's1', target: 'u1', type: EDGE_TYPE }];
|
||||
|
||||
const run = (updateList: any[]) =>
|
||||
checkWorkflowNodeAndConnection({
|
||||
nodes: [startNode, makeVarUpdateNode(updateList)],
|
||||
edges: connectedEdges
|
||||
});
|
||||
|
||||
it('flags empty updateList', () => {
|
||||
expect(run([])).toEqual(['u1']);
|
||||
});
|
||||
|
||||
it('flags row with empty variable', () => {
|
||||
expect(
|
||||
run([
|
||||
{
|
||||
variable: ['', ''],
|
||||
value: ['', 'hello'],
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
renderType: FlowNodeInputTypeEnum.input
|
||||
}
|
||||
])
|
||||
).toEqual(['u1']);
|
||||
});
|
||||
|
||||
it('flags input row with empty value', () => {
|
||||
expect(
|
||||
run([
|
||||
{
|
||||
variable: [VARIABLE_NODE_ID, 'foo'],
|
||||
value: ['', ''],
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
renderType: FlowNodeInputTypeEnum.input
|
||||
}
|
||||
])
|
||||
).toEqual(['u1']);
|
||||
});
|
||||
|
||||
it('passes when boolean input has no value (booleanMode decides)', () => {
|
||||
expect(
|
||||
run([
|
||||
{
|
||||
variable: [VARIABLE_NODE_ID, 'foo'],
|
||||
value: undefined,
|
||||
valueType: WorkflowIOValueTypeEnum.boolean,
|
||||
booleanMode: 'true',
|
||||
renderType: FlowNodeInputTypeEnum.input
|
||||
}
|
||||
])
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('passes when array clear mode has no value', () => {
|
||||
expect(
|
||||
run([
|
||||
{
|
||||
variable: [VARIABLE_NODE_ID, 'foo'],
|
||||
value: undefined,
|
||||
valueType: WorkflowIOValueTypeEnum.arrayString,
|
||||
arrayMode: 'clear',
|
||||
renderType: FlowNodeInputTypeEnum.input
|
||||
}
|
||||
])
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('flags reference row with incomplete value', () => {
|
||||
expect(
|
||||
run([
|
||||
{
|
||||
variable: [VARIABLE_NODE_ID, 'foo'],
|
||||
value: ['n2', ''],
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
renderType: FlowNodeInputTypeEnum.reference
|
||||
}
|
||||
])
|
||||
).toEqual(['u1']);
|
||||
});
|
||||
|
||||
it('flags reference array row with empty array', () => {
|
||||
expect(
|
||||
run([
|
||||
{
|
||||
variable: [VARIABLE_NODE_ID, 'foo'],
|
||||
value: [],
|
||||
valueType: WorkflowIOValueTypeEnum.arrayString,
|
||||
renderType: FlowNodeInputTypeEnum.reference
|
||||
}
|
||||
])
|
||||
).toEqual(['u1']);
|
||||
});
|
||||
|
||||
it('passes a fully filled input row', () => {
|
||||
expect(
|
||||
run([
|
||||
{
|
||||
variable: [VARIABLE_NODE_ID, 'foo'],
|
||||
value: ['', 'hello'],
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
renderType: FlowNodeInputTypeEnum.input
|
||||
}
|
||||
])
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('flags variable pointing to a non-existent node id', () => {
|
||||
expect(
|
||||
run([
|
||||
{
|
||||
variable: ['ghost-node', 'out1'],
|
||||
value: ['', 'hello'],
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
renderType: FlowNodeInputTypeEnum.input
|
||||
}
|
||||
])
|
||||
).toEqual(['u1']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user