Commit bbee34bc by YeYuheng Committed by GitHub

fix: filter workflow debug inputs (#7062)

* fix: filter workflow debug inputs

* fix: debug

* add test

---------

Co-authored-by: archer <545436317@qq.com>
parent 661cda83
......@@ -5,7 +5,7 @@ import {
type StoreEdgeItemType
} from '@fastgpt/global/core/workflow/type/edge';
import { useCallback, useState, useMemo } from 'react';
import { checkWorkflowNodeAndConnection } from '@/web/core/workflow/utils';
import { checkWorkflowNodeAndConnection, getNodeAllSource } from '@/web/core/workflow/utils';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { uiWorkflow2StoreWorkflow } from '../../utils';
import { type RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
......@@ -14,7 +14,6 @@ import dynamic from 'next/dynamic';
import { Box, Button, Flex } from '@chakra-ui/react';
import { type FieldErrors, useForm } from 'react-hook-form';
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import { nodeInputIsReference } from '@fastgpt/global/core/workflow/utils';
import { useContextSelector } from 'use-context-selector';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppContext } from '../../../context';
......@@ -29,6 +28,7 @@ import { useSafeTranslation } from '@fastgpt/web/hooks/useSafeTranslation';
import { WorkflowActionsContext } from '../../context/workflowActionsContext';
import { WorkflowDebugContext } from '../../context/workflowDebugContext';
import {
checkInputShouldRenderInDebug,
getDebugInputFormProps,
getDebugInputFormValue,
getDebugRuntimeInputs
......@@ -50,6 +50,12 @@ export const useDebug = () => {
const setNodes = useContextSelector(WorkflowBufferDataContext, (v) => v.setNodes);
const getNodes = useContextSelector(WorkflowBufferDataContext, (v) => v.getNodes);
const edges = useContextSelector(WorkflowBufferDataContext, (v) => v.edges);
const systemConfigNode = useContextSelector(WorkflowBufferDataContext, (v) => v.systemConfigNode);
const getNodeById = useContextSelector(WorkflowBufferDataContext, (v) => v.getNodeById);
const childrenNodeIdListMap = useContextSelector(
WorkflowBufferDataContext,
(v) => v.childrenNodeIdListMap
);
const { onUpdateNodeError, onRemoveError } = useContextSelector(WorkflowActionsContext, (v) => v);
const onStartNodeDebug = useContextSelector(WorkflowDebugContext, (v) => v.onStartNodeDebug);
......@@ -150,11 +156,20 @@ export const useDebug = () => {
const runtimeNode = runtimeNodes.find((node) => node.nodeId === runtimeNodeId);
if (!runtimeNode) return <></>;
// BUG: 工具调用的情况下,无法填写非必填
const referenceSourceNodes = getNodeAllSource({
nodeId: runtimeNode.nodeId,
systemConfigNode,
getNodeById,
edges,
chatConfig: appDetail.chatConfig,
t,
childrenNodeIdListMap
});
const renderInputs = runtimeNode.inputs.filter((input) => {
if (runtimeNode.flowNodeType === FlowNodeTypeEnum.pluginInput) return true;
if (nodeInputIsReference(input)) return true;
if (!input.value) return true;
return checkInputShouldRenderInDebug(input, {
showAllInputs: runtimeNode.flowNodeType === FlowNodeTypeEnum.pluginInput,
referenceSourceNodes
});
});
const variablesForm = useForm<Record<string, any>>({
......@@ -245,7 +260,7 @@ export const useDebug = () => {
<LabelAndFormRender
{...inputProps}
key={item.key}
label={item.label}
label={item.debugLabel || item.label}
required={item.required}
description={t(item.placeholder || item.description)}
inputType={nodeInputTypeToInputType(item.renderTypeList)}
......@@ -313,7 +328,12 @@ export const useDebug = () => {
internalVar,
filteredVar,
runtimeNodeId,
onStartNodeDebug
onStartNodeDebug,
systemConfigNode,
getNodeById,
edges,
appDetail.chatConfig,
childrenNodeIdListMap
]);
return {
......
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { nodeInputIsReference } from '@fastgpt/global/core/workflow/utils';
import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import {
workflowReferenceValueIsSelectable,
type WorkflowReferenceSourceNode
} from '@/web/core/workflow/utils';
const primitiveValueTypes = new Set<WorkflowIOValueTypeEnum>([
WorkflowIOValueTypeEnum.string,
......@@ -8,10 +12,49 @@ const primitiveValueTypes = new Set<WorkflowIOValueTypeEnum>([
WorkflowIOValueTypeEnum.boolean
]);
const inputReferenceValueIsValid = ({
input,
referenceSourceNodes = []
}: {
input: FlowNodeInputItemType;
referenceSourceNodes?: WorkflowReferenceSourceNode[];
}) => {
return workflowReferenceValueIsSelectable({
value: input.value,
sourceNodes: referenceSourceNodes,
valueType: input.valueType
});
};
/**
* 节点调试只补两类输入:插件入口的全部参数,以及普通节点里引用上游输出的参数。
* 其他已配置好的节点参数保持原值,不因空值或默认值额外展示调试输入框。
*/
export const checkInputShouldRenderInDebug = (
input: FlowNodeInputItemType,
options?: {
showAllInputs?: boolean;
referenceSourceNodes?: WorkflowReferenceSourceNode[];
}
) => {
if (options?.showAllInputs) return true;
if (
nodeInputIsReference(input) &&
inputReferenceValueIsValid({
input,
referenceSourceNodes: options?.referenceSourceNodes
})
) {
return true;
}
return false;
};
export const getDebugInputFormValue = (input: FlowNodeInputItemType) => {
if (nodeInputIsReference(input)) return undefined;
const value = input.value ?? input.defaultValue;
const value = input.value;
if (typeof value === 'object' && value !== null) {
return JSON.stringify(value, null, 2);
}
......
......@@ -2,12 +2,9 @@ import React, { useCallback, useEffect, useMemo } from 'react';
import type { RenderInputProps } from '../type';
import { Flex, Box, type ButtonProps, Grid } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { getNodeAllSource, filterWorkflowNodeOutputsByType } from '@/web/core/workflow/utils';
import { getNodeAllSource, filterSelectableWorkflowNodeOutputs } from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import {
NodeOutputKeyEnum,
WorkflowIOValueTypeEnum
} from '@fastgpt/global/core/workflow/constants';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import type {
ReferenceArrayValueType,
ReferenceItemValueType,
......@@ -15,10 +12,7 @@ import type {
} from '@fastgpt/global/core/workflow/type/io';
import dynamic from 'next/dynamic';
import { useContextSelector } from 'use-context-selector';
import {
FlowNodeOutputTypeEnum,
isNestedParentNodeType
} from '@fastgpt/global/core/workflow/node/constant';
import { isNestedParentNodeType } from '@fastgpt/global/core/workflow/node/constant';
import { AppContext } from '@/pageComponents/app/detail/context';
import {
WorkflowBufferDataContext,
......@@ -103,20 +97,17 @@ export const useReference = ({
</Flex>
),
value: node.nodeId,
children: filterWorkflowNodeOutputsByType(node.outputs, valueType)
.filter((output) => {
if (output.type === FlowNodeOutputTypeEnum.error) {
return node.catchError === true;
}
return output.id !== NodeOutputKeyEnum.addOutputParam && output.invalid !== true;
})
.map((output) => {
return {
label: t(output.label as any),
value: output.id,
valueType: output.valueType
};
})
children: filterSelectableWorkflowNodeOutputs({
outputs: node.outputs,
valueType,
catchError: node.catchError
}).map((output) => {
return {
label: t(output.label as any),
value: output.id,
valueType: output.valueType
};
})
};
})
.filter((item) => item.children.length > 0);
......
......@@ -27,7 +27,8 @@ import { type TFunction } from 'next-i18next';
import {
type FlowNodeInputItemType,
type FlowNodeOutputItemType,
type ReferenceItemValueType
type ReferenceItemValueType,
type ReferenceValueType
} from '@fastgpt/global/core/workflow/type/io';
import { type IfElseListItemType } from '@fastgpt/global/core/workflow/template/system/ifElse/type';
import { LoopRunModeEnum } from '@fastgpt/global/core/workflow/template/system/loopRun/loopRun';
......@@ -386,6 +387,93 @@ export const filterWorkflowNodeOutputsByType = (
);
};
export type WorkflowReferenceSourceNode = {
nodeId: string;
outputs: FlowNodeOutputItemType[];
catchError?: boolean;
};
/**
* 过滤引用选择器中真正可选的输出。
* ReferenceSelector 和节点 debug 的引用有效性判断必须共用这套规则,避免已删除、类型不匹配、
* addOutputParam、invalid output 或未开启 catchError 的错误输出在不同入口表现不一致。
*/
export const filterSelectableWorkflowNodeOutputs = ({
outputs,
valueType,
catchError
}: {
outputs: FlowNodeOutputItemType[];
valueType?: WorkflowIOValueTypeEnum;
catchError?: boolean;
}) => {
return filterWorkflowNodeOutputsByType(outputs, valueType ?? WorkflowIOValueTypeEnum.any).filter(
(output) => {
if (output.type === FlowNodeOutputTypeEnum.error) {
return catchError === true;
}
return output.id !== NodeOutputKeyEnum.addOutputParam && output.invalid !== true;
}
);
};
const referenceItemIsSelectable = ({
value,
sourceNodes,
valueType
}: {
value: ReferenceItemValueType;
sourceNodes: WorkflowReferenceSourceNode[];
valueType?: WorkflowIOValueTypeEnum;
}) => {
const [sourceNodeId, outputId] = value;
if (!sourceNodeId || !outputId) return false;
const sourceNode = sourceNodes.find((node) => node.nodeId === sourceNodeId);
if (!sourceNode) return false;
return filterSelectableWorkflowNodeOutputs({
outputs: sourceNode.outputs,
valueType,
catchError: sourceNode.catchError
}).some((output) => output.id === outputId);
};
/**
* 判断引用值是否仍能被 ReferenceSelector 选中。
* 单选引用要求当前二元组命中;多选引用只要存在一个仍可选的引用项,选择器就会展示有效值。
*/
export const workflowReferenceValueIsSelectable = ({
value,
sourceNodes,
valueType
}: {
value?: ReferenceValueType;
sourceNodes: WorkflowReferenceSourceNode[];
valueType?: WorkflowIOValueTypeEnum;
}) => {
if (!Array.isArray(value)) return false;
if (typeof value[0] === 'string') {
return referenceItemIsSelectable({
value: value as ReferenceItemValueType,
sourceNodes,
valueType
});
}
return value.some((item) => {
if (!Array.isArray(item)) return false;
return referenceItemIsSelectable({
value: item as ReferenceItemValueType,
sourceNodes,
valueType
});
});
};
export const getNodeAllSource = ({
nodeId,
systemConfigNode,
......
import { describe, expect, it } from 'vitest';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import type { WorkflowReferenceSourceNode } from '@/web/core/workflow/utils';
import {
checkInputShouldRenderInDebug,
getDebugInputFormProps,
getDebugInputFormValue,
getDebugRuntimeInputs
......@@ -16,7 +21,149 @@ const makeInput = (input: Partial<FlowNodeInputItemType>): FlowNodeInputItemType
...input
});
const validReferenceContext = {
referenceSourceNodes: [
{
nodeId: 'source',
outputs: [
{
id: 'text',
key: 'text',
label: 'Text',
type: FlowNodeOutputTypeEnum.static,
valueType: WorkflowIOValueTypeEnum.string
}
]
}
] satisfies WorkflowReferenceSourceNode[]
};
describe('useDebugInput', () => {
it('should render reference inputs in node debug form', () => {
const input = makeInput({
key: 'userChatInput',
renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.textarea],
selectedTypeIndex: 0,
value: [['source', 'text']]
});
expect(checkInputShouldRenderInDebug(input, validReferenceContext)).toBe(true);
});
it('should render reference config inputs in node debug form', () => {
const input = makeInput({
key: 'datasetSelectList',
renderTypeList: [
FlowNodeInputTypeEnum.reference,
FlowNodeInputTypeEnum.selectDatasetParamsModal
],
selectedTypeIndex: 0,
value: [['source', 'text']]
});
expect(checkInputShouldRenderInDebug(input, validReferenceContext)).toBe(true);
});
it('should not render reference inputs without selected reference value', () => {
const input = makeInput({
key: 'userChatInput',
renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.textarea],
selectedTypeIndex: 0,
value: []
});
expect(checkInputShouldRenderInDebug(input, validReferenceContext)).toBe(false);
});
it('should not render reference inputs with incomplete reference value', () => {
const input = makeInput({
key: 'userChatInput',
renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.textarea],
selectedTypeIndex: 0,
value: [['workflowStart', '']]
});
expect(checkInputShouldRenderInDebug(input, validReferenceContext)).toBe(false);
});
it('should not render reference inputs when source node is missing', () => {
const input = makeInput({
key: 'userChatInput',
renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.textarea],
selectedTypeIndex: 0,
value: [['deletedNode', 'text']]
});
expect(checkInputShouldRenderInDebug(input, validReferenceContext)).toBe(false);
});
it('should not render reference inputs when source output is missing', () => {
const input = makeInput({
key: 'userChatInput',
renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.textarea],
selectedTypeIndex: 0,
value: [['source', 'deletedOutput']]
});
expect(checkInputShouldRenderInDebug(input, validReferenceContext)).toBe(false);
});
it('should not render reference inputs when source output type cannot be selected', () => {
const input = makeInput({
key: 'userChatInput',
renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.textarea],
selectedTypeIndex: 0,
valueType: WorkflowIOValueTypeEnum.number,
value: [['source', 'text']]
});
expect(checkInputShouldRenderInDebug(input, validReferenceContext)).toBe(false);
});
it('should not render non-reference inputs in node debug form', () => {
const input = makeInput({
key: 'datasetSearchUsingExtensionQuery',
renderTypeList: [FlowNodeInputTypeEnum.textarea],
valueType: WorkflowIOValueTypeEnum.boolean,
value: undefined
});
expect(checkInputShouldRenderInDebug(input, validReferenceContext)).toBe(false);
});
it('should render all plugin input fields', () => {
const input = makeInput({
key: 'query',
renderTypeList: [FlowNodeInputTypeEnum.textarea],
valueType: WorkflowIOValueTypeEnum.string,
value: 'fixed value'
});
expect(checkInputShouldRenderInDebug(input, { showAllInputs: true })).toBe(true);
});
it('should render plugin input reference fields even without selected reference value', () => {
const input = makeInput({
key: 'query',
renderTypeList: [FlowNodeInputTypeEnum.reference],
selectedTypeIndex: 0,
value: []
});
expect(checkInputShouldRenderInDebug(input, { showAllInputs: true })).toBe(true);
});
it('should not render default values as missing debug inputs', () => {
const input = makeInput({
key: 'query',
renderTypeList: [FlowNodeInputTypeEnum.textarea],
valueType: WorkflowIOValueTypeEnum.arrayString,
defaultValue: ['default query']
});
expect(checkInputShouldRenderInDebug(input, validReferenceContext)).toBe(false);
});
it('should not use reference value as node debug form default value', () => {
const input = makeInput({
key: 'userChatInput',
......@@ -40,6 +187,16 @@ describe('useDebugInput', () => {
expect(props).not.toHaveProperty('defaultValue');
});
it('should not use default value as node debug form default value', () => {
const input = makeInput({
key: 'query',
renderTypeList: [FlowNodeInputTypeEnum.input],
defaultValue: 'default'
});
expect(getDebugInputFormValue(input)).toBeUndefined();
});
it('should clear old reference value when a rendered debug field is submitted empty', () => {
const referenceInput = makeInput({
key: 'userChatInput',
......
......@@ -18,10 +18,12 @@ import {
nodeTemplate2FlowNode,
storeNode2FlowNode,
filterWorkflowNodeOutputsByType,
filterSelectableWorkflowNodeOutputs,
workflowReferenceValueIsSelectable,
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';
import { NodeOutputKeyEnum, VARIABLE_NODE_ID } from '@fastgpt/global/core/workflow/constants';
describe('nodeTemplate2FlowNode', () => {
it('should convert template to flow node', () => {
......@@ -198,6 +200,154 @@ describe('filterWorkflowNodeOutputsByType', () => {
});
});
describe('filterSelectableWorkflowNodeOutputs', () => {
const makeOutput = (
id: string,
valueType: WorkflowIOValueTypeEnum,
extra?: Partial<FlowNodeOutputItemType>
): FlowNodeOutputItemType => ({
id,
key: id,
label: id,
type: FlowNodeOutputTypeEnum.static,
valueType,
...extra
});
it('filters outputs that cannot be selected by reference selector', () => {
const outputs: FlowNodeOutputItemType[] = [
makeOutput('text', WorkflowIOValueTypeEnum.string),
makeOutput('count', WorkflowIOValueTypeEnum.number),
makeOutput(NodeOutputKeyEnum.addOutputParam, WorkflowIOValueTypeEnum.string),
makeOutput('invalid', WorkflowIOValueTypeEnum.string, { invalid: true }),
makeOutput('error', WorkflowIOValueTypeEnum.string, { type: FlowNodeOutputTypeEnum.error })
];
const result = filterSelectableWorkflowNodeOutputs({
outputs,
valueType: WorkflowIOValueTypeEnum.string,
catchError: false
});
expect(result.map((output) => output.id)).toEqual(['text']);
});
it('keeps error output only when source node can catch error', () => {
const outputs: FlowNodeOutputItemType[] = [
makeOutput('text', WorkflowIOValueTypeEnum.string),
makeOutput('error', WorkflowIOValueTypeEnum.string, { type: FlowNodeOutputTypeEnum.error })
];
const result = filterSelectableWorkflowNodeOutputs({
outputs,
valueType: WorkflowIOValueTypeEnum.string,
catchError: true
});
expect(result.map((output) => output.id)).toEqual(['text', 'error']);
});
});
describe('workflowReferenceValueIsSelectable', () => {
const sourceNodes = [
{
nodeId: 'source',
outputs: [
{
id: 'text',
key: 'text',
label: 'text',
type: FlowNodeOutputTypeEnum.static,
valueType: WorkflowIOValueTypeEnum.string
},
{
id: 'count',
key: 'count',
label: 'count',
type: FlowNodeOutputTypeEnum.static,
valueType: WorkflowIOValueTypeEnum.number
}
]
}
];
it('returns true when single reference points to an existing selectable output', () => {
expect(
workflowReferenceValueIsSelectable({
value: ['source', 'text'],
sourceNodes,
valueType: WorkflowIOValueTypeEnum.string
})
).toBe(true);
});
it('returns false when referenced source node has been deleted', () => {
expect(
workflowReferenceValueIsSelectable({
value: ['deleted', 'text'],
sourceNodes,
valueType: WorkflowIOValueTypeEnum.string
})
).toBe(false);
});
it('returns false when referenced output no longer exists', () => {
expect(
workflowReferenceValueIsSelectable({
value: ['source', 'deleted'],
sourceNodes,
valueType: WorkflowIOValueTypeEnum.string
})
).toBe(false);
});
it('returns false when referenced output type is not selectable for current value type', () => {
expect(
workflowReferenceValueIsSelectable({
value: ['source', 'count'],
sourceNodes,
valueType: WorkflowIOValueTypeEnum.string
})
).toBe(false);
});
it('returns false for incomplete reference value', () => {
expect(
workflowReferenceValueIsSelectable({
value: ['source', ''],
sourceNodes,
valueType: WorkflowIOValueTypeEnum.string
})
).toBe(false);
});
it('returns true for multiple references when at least one item is selectable', () => {
expect(
workflowReferenceValueIsSelectable({
value: [
['deleted', 'text'],
['source', 'text']
],
sourceNodes,
valueType: WorkflowIOValueTypeEnum.string
})
).toBe(true);
});
it('returns false for multiple references when none of the items are selectable', () => {
expect(
workflowReferenceValueIsSelectable({
value: [
['deleted', 'text'],
['source', 'deleted']
],
sourceNodes,
valueType: WorkflowIOValueTypeEnum.string
})
).toBe(false);
});
});
describe('checkWorkflowNodeAndConnection', () => {
it('should validate nodes and connections', () => {
const nodes: Node[] = [
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment