diff --git a/packages/global/core/workflow/template/system/variableUpdate/constants.ts b/packages/global/core/workflow/template/system/variableUpdate/constants.ts new file mode 100644 index 000000000000..81d4a319e709 --- /dev/null +++ b/packages/global/core/workflow/template/system/variableUpdate/constants.ts @@ -0,0 +1,10 @@ +export enum VariableUpdateOperatorEnum { + set = 'set', + add = 'add', + sub = 'sub', + mul = 'mul', + div = 'div', + negate = 'negate', + push = 'push', + clear = 'clear' +} diff --git a/packages/global/core/workflow/template/system/variableUpdate/index.tsx b/packages/global/core/workflow/template/system/variableUpdate/index.tsx index a006b9594136..4d49954301a5 100644 --- a/packages/global/core/workflow/template/system/variableUpdate/index.tsx +++ b/packages/global/core/workflow/template/system/variableUpdate/index.tsx @@ -30,7 +30,8 @@ export const VariableUpdateNode: FlowNodeTemplateType = { value: [ { variable: ['', ''], - value: ['', ''], + updateType: 'set', + inputValue: '', valueType: WorkflowIOValueTypeEnum.string, renderType: FlowNodeInputTypeEnum.input } diff --git a/packages/global/core/workflow/template/system/variableUpdate/type.d.ts b/packages/global/core/workflow/template/system/variableUpdate/type.d.ts deleted file mode 100644 index 1468faedac17..000000000000 --- a/packages/global/core/workflow/template/system/variableUpdate/type.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { FlowNodeInputTypeEnum } from '../../../node/constant'; -import type { ReferenceItemValueType, ReferenceValueType } from '../../..//type/io'; -import type { WorkflowIOValueTypeEnum } from '../../../constants'; - -export type TUpdateListItem = { - variable?: ReferenceItemValueType; - value?: ReferenceValueType; // input: ['',value], reference: [nodeId,outputId] - valueType?: WorkflowIOValueTypeEnum; - renderType: FlowNodeInputTypeEnum.input | FlowNodeInputTypeEnum.reference; -}; diff --git a/packages/global/core/workflow/template/system/variableUpdate/type.ts b/packages/global/core/workflow/template/system/variableUpdate/type.ts new file mode 100644 index 000000000000..059daaf698ac --- /dev/null +++ b/packages/global/core/workflow/template/system/variableUpdate/type.ts @@ -0,0 +1,24 @@ +import { z } from 'zod'; +import { FlowNodeInputTypeEnum } from '../../../node/constant'; +import { WorkflowIOValueTypeEnum } from '../../../constants'; +import { VariableUpdateOperatorEnum } from './constants'; + +const ReferenceItemValueSchema = z.tuple([z.string(), z.union([z.string(), z.undefined()])]); + +export const UpdateListItemSchema = z.object({ + variable: ReferenceItemValueSchema.optional(), + valueType: z.enum(WorkflowIOValueTypeEnum).optional(), + renderType: z.union([ + z.literal(FlowNodeInputTypeEnum.input), + z.literal(FlowNodeInputTypeEnum.reference) + ]), + + updateType: z.enum(VariableUpdateOperatorEnum).optional(), + referenceValue: ReferenceItemValueSchema.optional(), + inputValue: z.any().optional(), + + /** @deprecated 旧格式字段,运行时由 normalizeUpdateItem 转换为 updateType + inputValue/referenceValue */ + value: z.any().optional() +}); + +export type TUpdateListItem = z.infer; diff --git a/packages/global/core/workflow/template/system/variableUpdate/utils.ts b/packages/global/core/workflow/template/system/variableUpdate/utils.ts new file mode 100644 index 000000000000..6d8de866c8d3 --- /dev/null +++ b/packages/global/core/workflow/template/system/variableUpdate/utils.ts @@ -0,0 +1,28 @@ +import type { TUpdateListItem } from './type'; +import { VariableUpdateOperatorEnum } from './constants'; + +export const normalizeUpdateItem = (item: TUpdateListItem): TUpdateListItem => { + // 新格式:已有 updateType,直接返回 + if (item.updateType !== undefined) return item; + + // 旧格式:value 是数组 + const raw = item.value; + if (Array.isArray(raw)) { + const [first, second] = raw as [string, string | undefined]; + const isRef = !!first; + return { + variable: item.variable, + valueType: item.valueType, + renderType: item.renderType, + updateType: VariableUpdateOperatorEnum.set, + referenceValue: isRef ? [first, second] : undefined, + inputValue: isRef ? undefined : second + }; + } + + return { + ...item, + updateType: VariableUpdateOperatorEnum.set, + inputValue: '' + }; +}; diff --git a/packages/global/core/workflow/utils.ts b/packages/global/core/workflow/utils.ts index 4e420c384723..28a47000baf0 100644 --- a/packages/global/core/workflow/utils.ts +++ b/packages/global/core/workflow/utils.ts @@ -336,13 +336,29 @@ export const toolSetData2FlowNodeIO = ({ nodes }: { nodes: StoreNodeItemType[] } }; }; -export const formatEditorVariablePickerIcon = ( - variables: { key: string; label: string; type?: `${VariableInputEnum}`; required?: boolean }[] +export const formatEditorVariable = ( + variables: { + key: string; + label: string; + type?: `${VariableInputEnum}`; + required?: boolean; + valueType?: WorkflowIOValueTypeEnum; + }[] ): EditorVariablePickerType[] => { - return variables.map((item) => ({ - ...item, - icon: item.type ? variableMap[item.type]?.icon : variableMap['input'].icon - })); + return variables.map((item) => { + const config = item.type ? variableMap[item.type] : variableMap['input']; + + return { + ...item, + icon: config?.icon, + valueType: + ([VariableInputEnum.custom, VariableInputEnum.internal].includes( + item.type as VariableInputEnum + ) && + item.valueType) || + config?.defaultValueType + }; + }); }; // Check the value is a valid reference value format: [variableId, outputId] diff --git a/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts b/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts index c0fc0373956a..40e5f996cd6b 100644 --- a/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts +++ b/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts @@ -1,5 +1,5 @@ import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; -import { VARIABLE_NODE_ID } from '@fastgpt/global/core/workflow/constants'; +import { VARIABLE_NODE_ID, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; import { DispatchNodeResponseKeyEnum, SseResponseEventEnum @@ -10,10 +10,30 @@ import { replaceEditorVariable } from '@fastgpt/global/core/workflow/runtime/utils'; import { type TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type'; +import type { ReferenceValueType } from '@fastgpt/global/core/workflow/type/io'; import { type ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; import { runtimeSystemVar2StoreType } from '../utils'; import { isValidReferenceValue } from '@fastgpt/global/core/workflow/utils'; import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils'; +import { VariableUpdateOperatorEnum } from '@fastgpt/global/core/workflow/template/system/variableUpdate/constants'; +import { normalizeUpdateItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/utils'; +import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; + +const operatorHandlerMap: Record any> = { + [VariableUpdateOperatorEnum.set]: (_cur, operand) => operand, + [VariableUpdateOperatorEnum.add]: (cur, operand) => Number(cur) + Number(operand), + [VariableUpdateOperatorEnum.sub]: (cur, operand) => Number(cur) - Number(operand), + [VariableUpdateOperatorEnum.mul]: (cur, operand) => Number(cur) * Number(operand), + [VariableUpdateOperatorEnum.div]: (cur, operand) => + Number(operand) !== 0 ? Number(cur) / Number(operand) : cur, + [VariableUpdateOperatorEnum.negate]: (cur) => + !(typeof cur === 'string' ? cur.toLowerCase() === 'true' : cur), + [VariableUpdateOperatorEnum.push]: (cur, operand) => [ + ...(Array.isArray(cur) ? cur : []), + operand + ], + [VariableUpdateOperatorEnum.clear]: () => [] +}; type Props = ModuleDispatchProps<{ [NodeInputKeyEnum.updateList]: TUpdateListItem[]; @@ -34,7 +54,8 @@ export const dispatchUpdateVariable = async (props: Props): Promise => const { updateList } = params; const nodeIds = runtimeNodes.map((node) => node.nodeId); - const result = updateList.map((item) => { + const result = updateList.map((rawItem) => { + const item = normalizeUpdateItem(rawItem); const variable = item.variable; if (!isValidReferenceValue(variable, nodeIds)) { @@ -49,33 +70,50 @@ export const dispatchUpdateVariable = async (props: Props): Promise => } const value = (() => { - // If first item is empty, it means it is a input value - if (!item.value?.[0]) { - const val = - typeof item.value?.[1] === 'string' - ? replaceEditorVariable({ - text: item.value?.[1], - nodes: runtimeNodes, - variables - }) - : item.value?.[1]; - - return valueTypeFormat(val, item.valueType); - } else { - return getReferenceVariableValue({ - value: item.value, - variables, - nodes: runtimeNodes - }); + const operator = item.updateType ?? VariableUpdateOperatorEnum.set; + const operand = + item.renderType === FlowNodeInputTypeEnum.reference + ? getReferenceVariableValue({ + value: item.referenceValue as ReferenceValueType, + variables, + nodes: runtimeNodes + }) + : replaceEditorVariable({ text: item.inputValue, nodes: runtimeNodes, variables }); + + const handler = operatorHandlerMap[operator]; + if (!handler) return operand ?? null; + + if (operator === VariableUpdateOperatorEnum.set) { + return valueTypeFormat(operand, item.valueType); } + + const currentValue = getReferenceVariableValue({ + value: variable, + variables, + nodes: runtimeNodes + }); + const typedCurrentValue = valueTypeFormat(currentValue, item.valueType) ?? currentValue; + + const processedOperand = + operator === VariableUpdateOperatorEnum.push && + typeof operand === 'string' && + (item.valueType === WorkflowIOValueTypeEnum.arrayObject || + item.valueType === WorkflowIOValueTypeEnum.arrayAny) + ? (() => { + try { + return JSON.parse(operand); + } catch { + return operand; + } + })() + : operand; + + return handler(typedCurrentValue, processedOperand); })(); - // Update node output - // Global variable if (varNodeId === VARIABLE_NODE_ID) { variables[varKey] = value; } else { - // Other nodes runtimeNodes .find((node) => node.nodeId === varNodeId) ?.outputs?.find((output) => { diff --git a/packages/web/i18n/en/workflow.json b/packages/web/i18n/en/workflow.json index ca3991ee4c15..932204aaf65a 100644 --- a/packages/web/i18n/en/workflow.json +++ b/packages/web/i18n/en/workflow.json @@ -228,6 +228,15 @@ "variable_description": "Variable description", "variable_picker_tips": "Type node name or variable name to search", "variable_update": "Variable Update", + "variable_update_operator_set": "Set", + "variable_update_operator_add": "Add", + "variable_update_operator_sub": "Subtract", + "variable_update_operator_mul": "Multiply", + "variable_update_operator_div": "Divide", + "variable_update_number_placeholder": "Enter value", + "variable_update_boolean_negate": "Negate", + "variable_update_operator_push": "Push", + "variable_update_operator_clear": "Clear", "workflow.My edit": "My Edit", "workflow.Switch_success": "Switch Successful", "workflow.Team cloud": "Team Cloud", diff --git a/packages/web/i18n/zh-CN/workflow.json b/packages/web/i18n/zh-CN/workflow.json index 3c815f06af8d..fa2bb5c6b501 100644 --- a/packages/web/i18n/zh-CN/workflow.json +++ b/packages/web/i18n/zh-CN/workflow.json @@ -228,6 +228,15 @@ "variable_description": "变量描述", "variable_picker_tips": "可输入节点名或变量名搜索", "variable_update": "变量更新", + "variable_update_operator_set": "等于", + "variable_update_operator_add": "加", + "variable_update_operator_sub": "减", + "variable_update_operator_mul": "乘", + "variable_update_operator_div": "除", + "variable_update_number_placeholder": "输入值", + "variable_update_boolean_negate": "取反", + "variable_update_operator_push": "追加", + "variable_update_operator_clear": "清空", "workflow.My edit": "我的编辑", "workflow.Switch_success": "切换成功", "workflow.Team cloud": "团队云端", diff --git a/packages/web/i18n/zh-Hant/workflow.json b/packages/web/i18n/zh-Hant/workflow.json index 513c5f4f5987..5129533866c8 100644 --- a/packages/web/i18n/zh-Hant/workflow.json +++ b/packages/web/i18n/zh-Hant/workflow.json @@ -228,6 +228,15 @@ "variable_description": "變數描述", "variable_picker_tips": "可以輸入節點名稱或變數名稱搜尋", "variable_update": "變數更新", + "variable_update_operator_set": "等於", + "variable_update_operator_add": "加", + "variable_update_operator_sub": "減", + "variable_update_operator_mul": "乘", + "variable_update_operator_div": "除", + "variable_update_number_placeholder": "輸入值", + "variable_update_boolean_negate": "取反", + "variable_update_operator_push": "追加", + "variable_update_operator_clear": "清空", "workflow.My edit": "我的編輯", "workflow.Switch_success": "切換成功", "workflow.Team cloud": "團隊雲端", diff --git a/projects/app/src/components/core/app/VariableEdit.tsx b/projects/app/src/components/core/app/VariableEdit.tsx index bf0289f5817a..3cb76f1ac5e9 100644 --- a/projects/app/src/components/core/app/VariableEdit.tsx +++ b/projects/app/src/components/core/app/VariableEdit.tsx @@ -19,7 +19,7 @@ import { import type { VariableItemType } from '@fastgpt/global/core/app/type.d'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { useTranslation } from 'next-i18next'; -import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils'; +import { formatEditorVariable } from '@fastgpt/global/core/workflow/utils'; import ChatFunctionTip from './Tip'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import MyIconButton from '@fastgpt/web/components/common/Icon/button'; @@ -77,7 +77,7 @@ const VariableEdit = ({ const [editingVariable, setEditingVariable] = useState(null); const formatVariables = useMemo(() => { - const results = formatEditorVariablePickerIcon(variables); + const results = formatEditorVariable(variables); return results.map((item) => { const variable = variables.find((variable) => variable.key === item.key)!; return { diff --git a/projects/app/src/components/core/app/formRender/index.tsx b/projects/app/src/components/core/app/formRender/index.tsx index 0da6ddf82b39..1af2c23ecac5 100644 --- a/projects/app/src/components/core/app/formRender/index.tsx +++ b/projects/app/src/components/core/app/formRender/index.tsx @@ -69,7 +69,7 @@ const InputRender = (props: InputRenderProps) => { variableLabels={props.variableLabels} title={props.title} maxLength={props.maxLength} - minH={40} + minH={Number(props.minH) || 40} maxH={120} isRichText={props.isRichText} /> @@ -85,7 +85,7 @@ const InputRender = (props: InputRenderProps) => { variableLabels={props.variableLabels} title={props.title} maxLength={props.maxLength} - minH={100} + minH={Number(props.minH) || 100} maxH={300} ExtensionPopover={props.ExtensionPopover} /> diff --git a/projects/app/src/pageComponents/app/detail/SimpleApp/EditForm.tsx b/projects/app/src/pageComponents/app/detail/SimpleApp/EditForm.tsx index 927fdd558f43..64e3504eadc7 100644 --- a/projects/app/src/pageComponents/app/detail/SimpleApp/EditForm.tsx +++ b/projects/app/src/pageComponents/app/detail/SimpleApp/EditForm.tsx @@ -19,7 +19,7 @@ import Avatar from '@fastgpt/web/components/common/Avatar'; import MyIcon from '@fastgpt/web/components/common/Icon'; import VariableEdit from '@/components/core/app/VariableEdit'; import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor'; -import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils'; +import { formatEditorVariable } from '@fastgpt/global/core/workflow/utils'; import SearchParamsTip from '@/components/core/dataset/SearchParamsTip'; import SettingLLMModel from '@/components/core/ai/SettingLLMModel'; import { TTSTypeEnum } from '@/web/core/app/constants'; @@ -86,7 +86,7 @@ const EditForm = ({ const formatVariables = useMemo( () => - formatEditorVariablePickerIcon([ + formatEditorVariable([ ...workflowSystemVariables.filter( (variable) => !['appId', 'chatId', 'responseChatItemId', 'histories'].includes(variable.key) diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeVariableUpdate.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeVariableUpdate.tsx index 4aebabb5c6f5..d8d267cf1de1 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeVariableUpdate.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeVariableUpdate.tsx @@ -1,25 +1,22 @@ -import React, { useCallback, useMemo, useRef } from 'react'; +import React, { useCallback, useMemo } from 'react'; import NodeCard from './render/NodeCard'; import { type NodeProps } from 'reactflow'; import { type FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; import { useTranslation } from 'next-i18next'; import { Box, Button, Flex } from '@chakra-ui/react'; +import NodeInputSelect from '@fastgpt/web/components/core/workflow/NodeInputSelect'; import { type TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type'; -import type { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; import { + WorkflowIOValueTypeEnum, NodeInputKeyEnum, VARIABLE_NODE_ID, VariableInputEnum } from '@fastgpt/global/core/workflow/constants'; import { useContextSelector } from 'use-context-selector'; -import { - FlowNodeInputMap, - FlowNodeInputTypeEnum -} from '@fastgpt/global/core/workflow/node/constant'; +import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import Container from '../components/Container'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { SmallAddIcon } from '@chakra-ui/icons'; -import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import { type ReferenceItemValueType, type ReferenceValueType @@ -39,6 +36,12 @@ import { InputTypeEnum } from '@/components/core/app/formRender/constant'; import { WorkflowActionsContext } from '../../context/workflowActionsContext'; import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance'; import { useMemoizedFn } from 'ahooks'; +import { VariableUpdateOperatorEnum } from '@fastgpt/global/core/workflow/template/system/variableUpdate/constants'; +import { normalizeUpdateItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/utils'; +import MySelect from '@fastgpt/web/components/common/MySelect'; +import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput'; +import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs'; +import ValueTypeLabel from './render/ValueTypeLabel'; const NodeVariableUpdate = ({ data, selected }: NodeProps) => { const { inputs = [], nodeId } = data; @@ -51,19 +54,6 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps) => ); const appDetail = useContextSelector(AppContext, (v) => v.appDetail); - const menuList = useRef([ - { - renderType: FlowNodeInputTypeEnum.input, - icon: FlowNodeInputMap[FlowNodeInputTypeEnum.input].icon, - label: t('common:core.workflow.inputType.Manual input') - }, - { - renderType: FlowNodeInputTypeEnum.reference, - icon: FlowNodeInputMap[FlowNodeInputTypeEnum.reference].icon, - label: t('common:core.workflow.inputType.Reference') - } - ]); - const variables = useMemoEnhance(() => { return getEditorVariables({ nodeId, @@ -84,7 +74,6 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps) => ); }, [feConfigs?.externalProviderWorkflowVariables]); - // Node inputs const updateList = useMemo( () => (inputs.find((input) => input.key === NodeInputKeyEnum.updateList) @@ -92,6 +81,44 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps) => [inputs] ); + const numberOperatorList = useMemo( + () => [ + { label: t('workflow:variable_update_operator_set'), value: VariableUpdateOperatorEnum.set }, + { label: t('workflow:variable_update_operator_add'), value: VariableUpdateOperatorEnum.add }, + { label: t('workflow:variable_update_operator_sub'), value: VariableUpdateOperatorEnum.sub }, + { label: t('workflow:variable_update_operator_mul'), value: VariableUpdateOperatorEnum.mul }, + { label: t('workflow:variable_update_operator_div'), value: VariableUpdateOperatorEnum.div } + ], + [t] + ); + + const booleanOperatorList = useMemo( + () => [ + { label: 'True', value: `${VariableUpdateOperatorEnum.set}:true` }, + { label: 'False', value: `${VariableUpdateOperatorEnum.set}:false` }, + { + label: t('workflow:variable_update_boolean_negate'), + value: VariableUpdateOperatorEnum.negate + } + ], + [t] + ); + + const arrayOperatorList = useMemo( + () => [ + { label: t('workflow:variable_update_operator_set'), value: VariableUpdateOperatorEnum.set }, + { + label: t('workflow:variable_update_operator_push'), + value: VariableUpdateOperatorEnum.push + }, + { + label: t('workflow:variable_update_operator_clear'), + value: VariableUpdateOperatorEnum.clear + } + ], + [t] + ); + const onUpdateList = useCallback( (value: TUpdateListItem[]) => { const updateListInput = inputs.find((input) => input.key === NodeInputKeyEnum.updateList); @@ -115,16 +142,13 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps) => const { inputType, formParams = {} } = (() => { const value = updateItem.variable; if (!value) { - return { - inputType: InputTypeEnum.input - }; + return { inputType: InputTypeEnum.input }; } // Global variables: 根据变量的 inputType 决定 if (value[0] === VARIABLE_NODE_ID) { const variableList = appDetail.chatConfig.variables || []; const variable = variableList.find((item) => item.key === value[1]); if (variable) { - // 文件类型在变量更新节点中使用文本框,因为不在运行时上下文中,无法使用文件选择器 const inputType = variable.type === VariableInputEnum.file ? InputTypeEnum.textarea @@ -133,7 +157,6 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps) => return { inputType, formParams: { - // 获取变量中一些表单配置 maxLength: variable.maxLength, minLength: variable.minLength, min: variable.min, @@ -157,68 +180,152 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps) => else if (value[0] && value[1]) { const output = getNodeById(value[0])?.outputs.find((output) => output.id === value[1]); if (output) { - return { - inputType: valueTypeToInputType(output.valueType) - }; + return { inputType: valueTypeToInputType(output.valueType) }; } } - return { - inputType: InputTypeEnum.input - }; + return { inputType: InputTypeEnum.input }; })(); + const { valueType } = getRefData({ variable: updateItem.variable, getNodeById, systemConfigNode, chatConfig: appDetail.chatConfig }); - const renderTypeData = menuList.current.find( - (item) => item.renderType === updateItem.renderType - ); - const onUpdateNewValue = (value: any) => { + const item = normalizeUpdateItem(updateItem); + const operator = item.updateType ?? VariableUpdateOperatorEnum.set; + + const onUpdateFields = (fields: Partial) => { + onUpdateList( + updateList.map((listItem, i) => (i === index ? { ...listItem, ...fields } : listItem)) + ); + }; + + const inputRenderProps = { + inputType, + ...formParams, + isRichText: false, + variables: [...variables, ...externalProviderWorkflowVariables], + variableLabels: variables, + value: item.inputValue, + onChange: (val: any) => onUpdateFields({ inputValue: val }), + minH: 80 + }; + + const renderValueInput = () => { if (updateItem.renderType === FlowNodeInputTypeEnum.reference) { - onUpdateList( - updateList.map((update, i) => (i === index ? { ...update, value: value } : update)) + return ( + + onUpdateFields({ referenceValue: refValue as ReferenceItemValueType }) + } + /> ); - } else { - onUpdateList( - updateList.map((update, i) => - i === index ? { ...update, value: ['', value] } : update - ) + } + + if (valueType === WorkflowIOValueTypeEnum.number) { + return ( + + + onUpdateFields({ updateType: op as VariableUpdateOperatorEnum }) + } + /> + onUpdateFields({ inputValue: val })} + /> + ); } + + if (valueType === WorkflowIOValueTypeEnum.boolean) { + return ( + + onUpdateFields( + val === VariableUpdateOperatorEnum.negate + ? { updateType: VariableUpdateOperatorEnum.negate, inputValue: undefined } + : { + updateType: VariableUpdateOperatorEnum.set, + inputValue: val.endsWith(':true') + } + ) + } + /> + ); + } + + if (valueType?.startsWith('array')) { + return ( + <> + { + const inputValue = + op === VariableUpdateOperatorEnum.clear + ? undefined + : op === VariableUpdateOperatorEnum.push && + valueType === WorkflowIOValueTypeEnum.arrayBoolean + ? true + : ''; + onUpdateFields({ updateType: op as VariableUpdateOperatorEnum, inputValue }); + }} + /> + {operator === VariableUpdateOperatorEnum.set && ( + + + + )} + {operator === VariableUpdateOperatorEnum.push && ( + + onUpdateFields({ inputValue: val })} + minH={80} + /> + + )} + + ); + } + + return ; }; return ( - {t('common:core.workflow.variable')} - { - onUpdateList( - updateList.map((update, i) => { - if (i === index) { - return { - ...update, - value: ['', ''], - valueType: getRefData({ - variable: value as ReferenceItemValueType, - getNodeById, - systemConfigNode, - chatConfig: appDetail.chatConfig - }).valueType, - variable: value as ReferenceItemValueType - }; - } - return update; - }) - ); - }} - /> + + {t('common:core.workflow.variable')} + {updateList.length > 1 && ( ) => cursor={'pointer'} _hover={{ color: 'red.500' }} position={'absolute'} - top={3} - right={3} + top={4} + right={4} onClick={() => { onUpdateList(updateList.filter((_, i) => i !== index)); }} /> )} - - - {t('common:value')} - item.renderType === updateItem.renderType)?.label - } - > - - - - {/* Render input components */} - {(() => { - if (updateItem.renderType === FlowNodeInputTypeEnum.reference) { - return ( - + + { + const newValueType = getRefData({ + variable: value as ReferenceItemValueType, + getNodeById, + systemConfigNode, + chatConfig: appDetail.chatConfig + }).valueType; + + onUpdateList( + updateList.map((listItem, i) => + i === index + ? { + ...listItem, + updateType: VariableUpdateOperatorEnum.set, + inputValue: newValueType === WorkflowIOValueTypeEnum.boolean ? true : '', + valueType: newValueType, + variable: value as ReferenceItemValueType + } + : listItem + ) ); - } + }} + labelSuffix={valueType ? : undefined} + /> + - return ( - - - - ); - })()} + + + {t('common:value')} + + + { + const isReference = e === FlowNodeInputTypeEnum.reference; + onUpdateFields({ + renderType: isReference + ? FlowNodeInputTypeEnum.reference + : FlowNodeInputTypeEnum.input, + updateType: VariableUpdateOperatorEnum.set, + referenceValue: undefined, + inputValue: isReference + ? undefined + : valueType === WorkflowIOValueTypeEnum.boolean + ? true + : '' + }); + }} + /> + + + + {renderValueInput()} + ); } @@ -309,7 +412,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps) => const Render = useMemo(() => { return ( - + {updateList.map((updateItem, index) => ( @@ -334,7 +437,8 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps) => ...updateList, { variable: ['', ''], - value: ['', ''], + updateType: VariableUpdateOperatorEnum.set, + inputValue: '', renderType: FlowNodeInputTypeEnum.input } ]); @@ -356,12 +460,14 @@ const VariableSelector = ({ nodeId, variable, valueType, - onSelect + onSelect, + labelSuffix }: { nodeId: string; variable?: ReferenceValueType; valueType?: WorkflowIOValueTypeEnum; onSelect: (e?: ReferenceValueType) => void; + labelSuffix?: React.ReactNode; }) => { const { t } = useTranslation(); @@ -377,6 +483,7 @@ const VariableSelector = ({ value={variable} onSelect={onSelect} isArray={valueType?.includes('array')} + labelSuffix={labelSuffix} /> ); }; diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeWorkflowStart.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeWorkflowStart.tsx index 18ac297a2212..3537b5860d46 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeWorkflowStart.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeWorkflowStart.tsx @@ -13,10 +13,7 @@ import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/const import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; import { AppContext } from '@/pageComponents/app/detail/context'; import { workflowSystemVariables } from '@/web/core/app/utils'; -import { - formatEditorVariablePickerIcon, - getAppChatConfig -} from '@fastgpt/global/core/workflow/utils'; +import { formatEditorVariable, getAppChatConfig } from '@fastgpt/global/core/workflow/utils'; import MyDivider from '@fastgpt/web/components/common/MyDivider'; import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance'; @@ -27,7 +24,7 @@ const NodeStart = ({ data, selected }: NodeProps) => { const systemConfigNode = useContextSelector(WorkflowBufferDataContext, (v) => v.systemConfigNode); const customGlobalVariables = useMemoEnhance(() => { - const globalVariables = formatEditorVariablePickerIcon( + const globalVariables = formatEditorVariable( getAppChatConfig({ chatConfig: appDetail.chatConfig, systemConfigNode, diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx index 595bfaac2dd7..30e979014700 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx @@ -52,6 +52,7 @@ type CommonSelectProps = { }[]; popDirection?: 'top' | 'bottom'; ButtonProps?: ButtonProps; + labelSuffix?: React.ReactNode; }; type SelectProps = CommonSelectProps & { isArray?: T; @@ -175,7 +176,8 @@ const SingleReferenceSelector = ({ list = [], onSelect, popDirection, - ButtonProps + ButtonProps, + labelSuffix }: SelectProps) => { const getSelectValue = useCallback( (value: ReferenceValueType) => { @@ -221,6 +223,7 @@ const SingleReferenceSelector = ({ {nodeName} {outputName} + {labelSuffix} ) : ( @@ -235,7 +238,7 @@ const SingleReferenceSelector = ({ ButtonProps={ButtonProps} /> ); - }, [ButtonProps, getSelectValue, list, onSelect, placeholder, popDirection, value]); + }, [ButtonProps, getSelectValue, labelSuffix, list, onSelect, placeholder, popDirection, value]); return ItemSelector; }; diff --git a/projects/app/src/web/core/workflow/utils.ts b/projects/app/src/web/core/workflow/utils.ts index 11fb72f32a40..f9458680e0a2 100644 --- a/projects/app/src/web/core/workflow/utils.ts +++ b/projects/app/src/web/core/workflow/utils.ts @@ -20,7 +20,7 @@ import { VARIABLE_NODE_ID, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/ import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { type EditorVariablePickerType } from '@fastgpt/web/components/common/Textarea/PromptEditor/type'; import { - formatEditorVariablePickerIcon, + formatEditorVariable, getAppChatConfig, getHandleId } from '@fastgpt/global/core/workflow/utils'; @@ -655,7 +655,7 @@ export const getWorkflowGlobalVariables = ({ systemConfigNode?: StoreNodeItemType; chatConfig: AppChatConfigType; }): EditorVariablePickerType[] => { - const globalVariables = formatEditorVariablePickerIcon( + const globalVariables = formatEditorVariable( getAppChatConfig({ chatConfig, systemConfigNode, diff --git a/test/cases/service/core/app/workflow/dispatch/runUpdateVar.test.ts b/test/cases/service/core/app/workflow/dispatch/runUpdateVar.test.ts new file mode 100644 index 000000000000..c1f1d9c2fdfd --- /dev/null +++ b/test/cases/service/core/app/workflow/dispatch/runUpdateVar.test.ts @@ -0,0 +1,726 @@ +import { describe, it, expect, vi } from 'vitest'; +import { dispatchUpdateVariable } from '@fastgpt/service/core/workflow/dispatch/tools/runUpdateVar'; +import { + VARIABLE_NODE_ID, + WorkflowIOValueTypeEnum, + NodeInputKeyEnum +} from '@fastgpt/global/core/workflow/constants'; +import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { VariableUpdateOperatorEnum } from '@fastgpt/global/core/workflow/template/system/variableUpdate/constants'; +import type { TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type'; +import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type'; + +/** 构造最小 mock props */ +const createMockProps = ({ + updateList, + variables = {}, + runtimeNodes = [] +}: { + updateList: TUpdateListItem[]; + variables?: Record; + runtimeNodes?: Partial[]; +}) => { + const allNodes = [ + { + nodeId: VARIABLE_NODE_ID, + name: 'Variables', + inputs: [], + outputs: [] + }, + ...runtimeNodes + ] as RuntimeNodeItemType[]; + + return { + chatConfig: { variables: [] }, + params: { [NodeInputKeyEnum.updateList]: updateList }, + variables, + runtimeNodes: allNodes, + runtimeEdges: [], + workflowStreamResponse: undefined, + externalProvider: { externalWorkflowVariables: [] }, + runningAppInfo: { + id: 'test-app', + teamId: 'test-team', + tmbId: 'test-tmb', + name: 'Test App', + isChildApp: true + }, + checkIsStopping: () => false, + mode: 'test' as const, + timezone: 'Asia/Shanghai', + runningUserInfo: { + username: 'test', + teamName: 'test', + memberName: 'test', + contact: '', + teamId: 'test-team', + tmbId: 'test-tmb' + }, + uid: 'test-uid', + chatId: 'test-chat', + histories: [], + query: [], + stream: false, + maxRunTimes: 100, + workflowDispatchDeep: 0, + node: allNodes[0], + mcpClientMemory: {} + } as any; +}; + +/** 构造全局变量更新项(新扁平格式) */ +const createGlobalVarItem = ( + varKey: string, + fields: Pick, + valueType: WorkflowIOValueTypeEnum, + renderType = FlowNodeInputTypeEnum.input +): TUpdateListItem => ({ + variable: [VARIABLE_NODE_ID, varKey], + valueType, + renderType, + ...fields +}); + +/** 构造节点输出变量更新项 */ +const createNodeOutputItem = ( + nodeId: string, + outputId: string, + fields: Pick, + valueType: WorkflowIOValueTypeEnum +): TUpdateListItem => ({ + variable: [nodeId, outputId], + valueType, + renderType: FlowNodeInputTypeEnum.input, + ...fields +}); + +describe('dispatchUpdateVariable - 运算操作符', () => { + // ============ 数字运算 ============ + describe('数字运算', () => { + it('set: 直接赋值为 42', async () => { + const variables = { score: 0 }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'score', + { updateType: VariableUpdateOperatorEnum.set, inputValue: 42 }, + WorkflowIOValueTypeEnum.number + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.score).toBe(42); + }); + + it('set: 空输入返回 null', async () => { + const variables = { score: 10 }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'score', + { updateType: VariableUpdateOperatorEnum.set, inputValue: '' }, + WorkflowIOValueTypeEnum.number + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.score).toBeNull(); + }); + + it('add: 原值 10 加 5 等于 15', async () => { + const variables = { score: 10 }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'score', + { updateType: VariableUpdateOperatorEnum.add, inputValue: 5 }, + WorkflowIOValueTypeEnum.number + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.score).toBe(15); + }); + + it('sub: 原值 10 减 3 等于 7', async () => { + const variables = { score: 10 }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'score', + { updateType: VariableUpdateOperatorEnum.sub, inputValue: 3 }, + WorkflowIOValueTypeEnum.number + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.score).toBe(7); + }); + + it('mul: 原值 10 乘 2 等于 20', async () => { + const variables = { score: 10 }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'score', + { updateType: VariableUpdateOperatorEnum.mul, inputValue: 2 }, + WorkflowIOValueTypeEnum.number + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.score).toBe(20); + }); + + it('div: 原值 10 除 4 等于 2.5', async () => { + const variables = { score: 10 }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'score', + { updateType: VariableUpdateOperatorEnum.div, inputValue: 4 }, + WorkflowIOValueTypeEnum.number + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.score).toBe(2.5); + }); + + it('div: 除以 0 保留原值', async () => { + const variables = { score: 10 }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'score', + { updateType: VariableUpdateOperatorEnum.div, inputValue: 0 }, + WorkflowIOValueTypeEnum.number + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.score).toBe(10); + }); + + it('add: 操作数为 NaN(undefined)时保留原值', async () => { + const variables = { score: 10 }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'score', + { updateType: VariableUpdateOperatorEnum.add, inputValue: undefined }, + WorkflowIOValueTypeEnum.number + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.score).toBe(10); + }); + + it('同一变量多次更新:+5 再 ×2,原值 10 → 15 → 30', async () => { + const variables = { score: 10 }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'score', + { updateType: VariableUpdateOperatorEnum.add, inputValue: 5 }, + WorkflowIOValueTypeEnum.number + ), + createGlobalVarItem( + 'score', + { updateType: VariableUpdateOperatorEnum.mul, inputValue: 2 }, + WorkflowIOValueTypeEnum.number + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.score).toBe(30); + }); + + it('变量未初始化(undefined)时作为 0 参与运算', async () => { + const variables: Record = {}; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'score', + { updateType: VariableUpdateOperatorEnum.add, inputValue: 5 }, + WorkflowIOValueTypeEnum.number + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.score).toBe(5); + }); + }); + + // ============ 布尔运算 ============ + describe('布尔运算', () => { + it('set: True', async () => { + const variables = { flag: false }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'flag', + { updateType: VariableUpdateOperatorEnum.set, inputValue: true }, + WorkflowIOValueTypeEnum.boolean + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.flag).toBe(true); + }); + + it('set: False', async () => { + const variables = { flag: true }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'flag', + { updateType: VariableUpdateOperatorEnum.set, inputValue: false }, + WorkflowIOValueTypeEnum.boolean + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.flag).toBe(false); + }); + + it('negate: 原值 true → false', async () => { + const variables = { flag: true }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'flag', + { updateType: VariableUpdateOperatorEnum.negate }, + WorkflowIOValueTypeEnum.boolean + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.flag).toBe(false); + }); + + it('negate: 原值 false → true', async () => { + const variables = { flag: false }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'flag', + { updateType: VariableUpdateOperatorEnum.negate }, + WorkflowIOValueTypeEnum.boolean + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.flag).toBe(true); + }); + + it('negate: 原值为字符串 "false" → true(字符串自动转换)', async () => { + const variables = { flag: 'false' }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'flag', + { updateType: VariableUpdateOperatorEnum.negate }, + WorkflowIOValueTypeEnum.boolean + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.flag).toBe(true); + }); + + it('negate: 原值为字符串 "true" → false(字符串自动转换)', async () => { + const variables = { flag: 'true' }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'flag', + { updateType: VariableUpdateOperatorEnum.negate }, + WorkflowIOValueTypeEnum.boolean + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.flag).toBe(false); + }); + + it('negate: 原值 undefined → true(!falsy = true)', async () => { + const variables: Record = {}; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'flag', + { updateType: VariableUpdateOperatorEnum.negate }, + WorkflowIOValueTypeEnum.boolean + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.flag).toBe(true); + }); + + it('连续取反两次恢复原值', async () => { + const variables = { flag: true }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'flag', + { updateType: VariableUpdateOperatorEnum.negate }, + WorkflowIOValueTypeEnum.boolean + ), + createGlobalVarItem( + 'flag', + { updateType: VariableUpdateOperatorEnum.negate }, + WorkflowIOValueTypeEnum.boolean + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.flag).toBe(true); + }); + }); + + // ============ 数组运算 ============ + describe('数组运算', () => { + it('set: 直接赋值数组', async () => { + const variables = { list: ['a', 'b'] }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'list', + { updateType: VariableUpdateOperatorEnum.set, inputValue: ['x', 'y'] }, + WorkflowIOValueTypeEnum.arrayString + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.list).toEqual(['x', 'y']); + }); + + it('push: 追加元素到末尾', async () => { + const variables = { list: ['a', 'b'] }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'list', + { updateType: VariableUpdateOperatorEnum.push, inputValue: 'x' }, + WorkflowIOValueTypeEnum.arrayString + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.list).toEqual(['a', 'b', 'x']); + }); + + it('clear: 清空数组', async () => { + const variables = { list: ['a', 'b'] }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'list', + { updateType: VariableUpdateOperatorEnum.clear }, + WorkflowIOValueTypeEnum.arrayString + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.list).toEqual([]); + }); + + it('push: 原值 undefined → [newItem]', async () => { + const variables: Record = {}; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'list', + { updateType: VariableUpdateOperatorEnum.push, inputValue: 'x' }, + WorkflowIOValueTypeEnum.arrayString + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.list).toEqual(['x']); + }); + + it('clear: 原值 undefined → []', async () => { + const variables: Record = {}; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'list', + { updateType: VariableUpdateOperatorEnum.clear }, + WorkflowIOValueTypeEnum.arrayString + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.list).toEqual([]); + }); + }); + + // ============ 旧数据兼容性(生产环境格式) ============ + describe('旧数据兼容性', () => { + it('旧格式数字 ["", 10] → 直接赋值 10', async () => { + const variables = { score: 0 }; + const props = createMockProps({ + updateList: [ + { + variable: [VARIABLE_NODE_ID, 'score'], + value: ['', 10], + valueType: WorkflowIOValueTypeEnum.number, + renderType: FlowNodeInputTypeEnum.input + } as any + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.score).toBe(10); + }); + + it('旧格式布尔 ["", true] → 赋值 true', async () => { + const variables = { flag: false }; + const props = createMockProps({ + updateList: [ + { + variable: [VARIABLE_NODE_ID, 'flag'], + value: ['', true], + valueType: WorkflowIOValueTypeEnum.boolean, + renderType: FlowNodeInputTypeEnum.input + } as any + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.flag).toBe(true); + }); + + it('旧格式布尔 ["", false] → 赋值 false', async () => { + const variables = { flag: true }; + const props = createMockProps({ + updateList: [ + { + variable: [VARIABLE_NODE_ID, 'flag'], + value: ['', false], + valueType: WorkflowIOValueTypeEnum.boolean, + renderType: FlowNodeInputTypeEnum.input + } as any + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.flag).toBe(false); + }); + + it('旧格式字符串 ["", "hello"] → 赋值 "hello"', async () => { + const variables = { text: '' }; + const props = createMockProps({ + updateList: [ + { + variable: [VARIABLE_NODE_ID, 'text'], + value: ['', 'hello'], + valueType: WorkflowIOValueTypeEnum.string, + renderType: FlowNodeInputTypeEnum.input + } as any + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.text).toBe('hello'); + }); + }); + + // ============ 节点输出变量 ============ + describe('节点输出变量', () => { + it('数字加法:更新节点输出', async () => { + const runtimeNodes: Partial[] = [ + { + nodeId: 'node1', + name: 'Node 1', + inputs: [], + outputs: [ + { + id: 'output1', + value: 10, + key: 'output1', + valueType: WorkflowIOValueTypeEnum.number, + label: 'output1', + type: 'static' as any + } + ] + } + ]; + const props = createMockProps({ + updateList: [ + createNodeOutputItem( + 'node1', + 'output1', + { updateType: VariableUpdateOperatorEnum.add, inputValue: 5 }, + WorkflowIOValueTypeEnum.number + ) + ], + runtimeNodes + }); + + await dispatchUpdateVariable(props); + const node = (props.runtimeNodes as RuntimeNodeItemType[]).find((n) => n.nodeId === 'node1'); + expect(node?.outputs?.find((o) => o.id === 'output1')?.value).toBe(15); + }); + + it('布尔取反:更新节点输出', async () => { + const runtimeNodes: Partial[] = [ + { + nodeId: 'node1', + name: 'Node 1', + inputs: [], + outputs: [ + { + id: 'flag', + value: true, + key: 'flag', + valueType: WorkflowIOValueTypeEnum.boolean, + label: 'flag', + type: 'static' as any + } + ] + } + ]; + const props = createMockProps({ + updateList: [ + createNodeOutputItem( + 'node1', + 'flag', + { updateType: VariableUpdateOperatorEnum.negate }, + WorkflowIOValueTypeEnum.boolean + ) + ], + runtimeNodes + }); + + await dispatchUpdateVariable(props); + const node = (props.runtimeNodes as RuntimeNodeItemType[]).find((n) => n.nodeId === 'node1'); + expect(node?.outputs?.find((o) => o.id === 'flag')?.value).toBe(false); + }); + }); + + // ============ 边界情况 ============ + describe('边界情况', () => { + it('未知 operator 兜底:返回 operand 值', async () => { + const variables = { score: 10 }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'score', + { updateType: 'unknown_op' as any, inputValue: 99 }, + WorkflowIOValueTypeEnum.number + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.score).toBe(99); + }); + + it('set + undefined(数字类型)→ 返回 undefined', async () => { + const variables = { score: 10 }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'score', + { updateType: VariableUpdateOperatorEnum.set, inputValue: undefined }, + WorkflowIOValueTypeEnum.number + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.score).toBeUndefined(); + }); + + it('push arrayObject:JSON 字符串 operand 自动解析为对象', async () => { + const variables = { items: [{ id: 1 }] }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'items', + { updateType: VariableUpdateOperatorEnum.push, inputValue: '{"id":2}' }, + WorkflowIOValueTypeEnum.arrayObject + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.items).toEqual([{ id: 1 }, { id: 2 }]); + }); + + it('push arrayString:字符串 operand 保持原样(不做 JSON 解析)', async () => { + const variables = { list: ['a'] }; + const props = createMockProps({ + updateList: [ + createGlobalVarItem( + 'list', + { updateType: VariableUpdateOperatorEnum.push, inputValue: '{"not":"parsed"}' }, + WorkflowIOValueTypeEnum.arrayString + ) + ], + variables + }); + + await dispatchUpdateVariable(props); + expect(variables.list).toEqual(['a', '{"not":"parsed"}']); + }); + }); +});