fix: node copy, debug variables, auto-execution (#5664)

* fix debug variables

* auto execute condition

* fix autoTTSResponse

* node copy

* memory debug nodes

* doc

* yuque doc

* fix: debug

* img

* i18n

---------

Co-authored-by: archer <545436317@qq.com>
This commit is contained in:
heheer
2025-09-17 22:29:56 +08:00
committed by GitHub
parent 1581a08082
commit ab29710945
96 changed files with 227 additions and 88 deletions

View File

@@ -53,7 +53,7 @@ const LabelAndFormRender = ({
return (
<Box _notLast={{ mb: 4 }}>
<Flex alignItems={'center'} mb={1}>
{typeof label === 'string' ? <FormLabel required={required}>{label}</FormLabel> : label}
{typeof label === 'string' ? <FormLabel required={required}>{t(label)}</FormLabel> : label}
{placeholder && <QuestionTip ml={1} label={placeholder} />}
</Flex>

View File

@@ -64,6 +64,7 @@ const ChatInput = ({
const chatInputGuide = useContextSelector(ChatBoxContext, (v) => v.chatInputGuide);
const fileSelectConfig = useContextSelector(ChatBoxContext, (v) => v.fileSelectConfig);
const dialogTips = useContextSelector(ChatBoxContext, (v) => v.dialogTips);
const autoTTSResponse = useContextSelector(ChatBoxContext, (v) => v.autoTTSResponse);
const fileCtrl = useFieldArray({
control,
@@ -448,7 +449,8 @@ const ChatInput = ({
handleSend={(text) => {
onSendMessage({
text: text.trim(),
files: fileList
files: fileList,
autoTTSResponse
});
replaceFiles([]);
}}

View File

@@ -46,7 +46,6 @@ export type ChatProviderProps = {
type useChatStoreType = ChatProviderProps & {
welcomeText: string;
variableList: VariableItemType[];
allVariableList: VariableItemType[];
questionGuide: AppQGConfigType;
ttsConfig: AppTTSConfigType;
whisperConfig: AppWhisperConfigType;
@@ -238,10 +237,7 @@ const Provider = ({
const value: useChatStoreType = {
...props,
welcomeText,
variableList: variables.filter(
(item) => item.type !== VariableInputEnum.custom && item.type !== VariableInputEnum.internal
),
allVariableList: variables,
variableList: variables,
questionGuide,
ttsConfig,
fileSelectConfig,

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo } from 'react';
import React, { useMemo } from 'react';
import { type UseFormReturn } from 'react-hook-form';
import { useTranslation } from 'next-i18next';
import { Box, Button, Card, Flex } from '@chakra-ui/react';
@@ -152,6 +152,19 @@ const VariableInput = ({
/>
);
})}
{!chatStarted && commonVariableList.length === 0 && (
<Button
leftIcon={<MyIcon name={'core/chat/chatFill'} w={'16px'} />}
size={'sm'}
maxW={'100px'}
mt={4}
onClick={variablesForm.handleSubmit(() => {
chatForm.setValue('chatStarted', true);
})}
>
{t('chat:start_chat')}
</Button>
)}
</Card>
</Box>
)}

View File

@@ -20,7 +20,12 @@ const ChatHomeVariablesForm = ({ chatForm }: Props) => {
const variablesForm = useContextSelector(ChatItemContext, (v) => v.variablesForm);
const variableList = useContextSelector(ChatBoxContext, (v) => v.variableList);
const allVariableList = useContextSelector(ChatBoxContext, (v) => v.allVariableList);
const externalVariableList = variableList.filter(
(item) => item.type === VariableInputEnum.custom
);
const commonVariableList = variableList.filter(
(item) => item.type !== VariableInputEnum.custom && item.type !== VariableInputEnum.internal
);
return (
<Card
@@ -31,27 +36,25 @@ const ChatHomeVariablesForm = ({ chatForm }: Props) => {
>
<Box p={3}>
{/* custom variables */}
{allVariableList.filter((i) => i.type === VariableInputEnum.custom).length > 0 && (
{externalVariableList.length > 0 && (
<>
{allVariableList
.filter((i) => i.type === VariableInputEnum.custom)
.map((item) => (
<LabelAndFormRender
{...item}
key={item.key}
fieldName={`variables.${item.key}`}
placeholder={item.description}
inputType={variableInputTypeToInputType(item.type, item.valueType)}
form={variablesForm}
bg={'myGray.50'}
/>
))}
{externalVariableList.map((item) => (
<LabelAndFormRender
{...item}
key={item.key}
fieldName={`variables.${item.key}`}
placeholder={item.description}
inputType={variableInputTypeToInputType(item.type, item.valueType)}
form={variablesForm}
bg={'myGray.50'}
/>
))}
</>
)}
{/* normal variables */}
{variableList.length > 0 && (
{commonVariableList.length > 0 && (
<>
{variableList.map((item) => (
{commonVariableList.map((item) => (
<LabelAndFormRender
{...item}
key={item.key}

View File

@@ -144,7 +144,6 @@ const ChatBox = ({
const outLinkAuthData = useContextSelector(ChatBoxContext, (v) => v.outLinkAuthData);
const welcomeText = useContextSelector(ChatBoxContext, (v) => v.welcomeText);
const variableList = useContextSelector(ChatBoxContext, (v) => v.variableList);
const allVariableList = useContextSelector(ChatBoxContext, (v) => v.allVariableList);
const questionGuide = useContextSelector(ChatBoxContext, (v) => v.questionGuide);
const startSegmentedAudio = useContextSelector(ChatBoxContext, (v) => v.startSegmentedAudio);
const finishSegmentedAudio = useContextSelector(ChatBoxContext, (v) => v.finishSegmentedAudio);
@@ -156,11 +155,14 @@ const ChatBox = ({
const isInteractive = useMemo(() => checkIsInteractiveByHistories(chatRecords), [chatRecords]);
const showExternalVariable = useMemo(() => {
return (
[ChatTypeEnum.log, ChatTypeEnum.test, ChatTypeEnum.chat].includes(chatType) &&
allVariableList.some((item) => item.type === VariableInputEnum.custom)
);
}, [allVariableList, chatType]);
const map: Record<string, boolean> = {
[ChatTypeEnum.log]: true,
[ChatTypeEnum.test]: true,
[ChatTypeEnum.chat]: true,
[ChatTypeEnum.home]: true
};
return map[chatType] && variableList.some((item) => item.type === VariableInputEnum.custom);
}, [variableList, chatType]);
// compute variable input is finish.
const chatForm = useForm<ChatBoxInputFormType>({
@@ -173,10 +175,20 @@ const ChatBox = ({
const { setValue, watch } = chatForm;
const chatStartedWatch = watch('chatStarted');
// 可以进入对话框对话
const commonVariableList = variableList.filter(
(item) => item.type !== VariableInputEnum.custom && item.type !== VariableInputEnum.internal
);
/*
对话已经开始的标记:
1. 保证 appId 一致。
2. 有对话记录/手动点了开始/默认没有需要填写的变量。
*/
const chatStarted =
chatBoxData?.appId === appId &&
(chatRecords.length > 0 || chatStartedWatch || variableList.length === 0);
(chatRecords.length > 0 ||
chatStartedWatch ||
(commonVariableList.length === 0 && !showExternalVariable));
// 滚动到底部
const scrollToBottom = useMemoizedFn((behavior: 'smooth' | 'auto' = 'smooth', delay = 0) => {
@@ -449,7 +461,7 @@ const ChatBox = ({
// Only declared variables are kept
const requestVariables: Record<string, any> = {};
allVariableList?.forEach((item) => {
variableList?.forEach((item) => {
const val =
variables[item.key] === '' ||
variables[item.key] === undefined ||
@@ -827,7 +839,7 @@ const ChatBox = ({
feConfigs?.show_emptyChat &&
showEmptyIntro &&
chatRecords.length === 0 &&
!variableList?.length &&
!commonVariableList?.length &&
!showExternalVariable &&
!welcomeText,
[
@@ -835,7 +847,7 @@ const ChatBox = ({
feConfigs?.show_emptyChat,
showEmptyIntro,
chatRecords.length,
variableList?.length,
commonVariableList?.length,
showExternalVariable,
welcomeText
]
@@ -1126,8 +1138,7 @@ const ChatBox = ({
>
<Flex h={'100%'} flexDir={'column'} justifyContent={'center'} w={'100%'}>
{HomeChatRenderBox}
{allVariableList.filter((item) => item.type !== VariableInputEnum.internal).length >
0 ? (
{variableList.filter((item) => item.type !== VariableInputEnum.internal).length > 0 ? (
<Box w={'100%'}>
<ChatHomeVariablesForm chatForm={chatForm} />
</Box>

View File

@@ -30,6 +30,7 @@ import {
nodeInputTypeToInputType,
variableInputTypeToInputType
} from '@/components/core/app/formRender/utils';
import { useSafeTranslation } from '@fastgpt/web/hooks/useSafeTranslation';
const MyRightDrawer = dynamic(
() => import('@fastgpt/web/components/common/MyDrawer/MyRightDrawer')
@@ -41,7 +42,7 @@ enum TabEnum {
}
export const useDebug = () => {
const { t } = useTranslation();
const { t } = useSafeTranslation();
const { toast } = useToast();
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
@@ -241,8 +242,8 @@ export const useDebug = () => {
<MyRightDrawer
onClose={onClose}
iconSrc="core/workflow/debugBlue"
title={t('common:core.workflow.Debug Node')}
maxW={['90vw', '35vw']}
title={t('workflow:debug_test')}
maxW={['90vw', '40vw']}
px={0}
>
<Box flex={'1 0 0'} overflow={'auto'} px={6}>
@@ -260,12 +261,27 @@ export const useDebug = () => {
onChange={setCurrentTab}
/>
)}
<Box display={currentTab === TabEnum.node ? 'block' : 'none'}>
{renderInputs.map((item) => (
<LabelAndFormRender
key={item.key}
label={item.label}
required={item.required}
placeholder={t(item.placeholder || item.description)}
inputType={nodeInputTypeToInputType(item.renderTypeList)}
form={variablesForm}
fieldName={`nodeVariables.${item.key}`}
bg={'myGray.50'}
/>
))}
</Box>
<Box display={currentTab === TabEnum.global ? 'block' : 'none'}>
{customVar.map((item) => (
<LabelAndFormRender
{...item}
key={item.key}
placeholder={item.description}
label={item.label}
required={item.required}
placeholder={t(item.description)}
inputType={variableInputTypeToInputType(item.type)}
form={variablesForm}
fieldName={`variables.${item.key}`}
@@ -274,9 +290,10 @@ export const useDebug = () => {
))}
{internalVar.map((item) => (
<LabelAndFormRender
{...item}
key={item.key}
placeholder={item.description}
label={item.label}
required={item.required}
placeholder={t(item.description)}
inputType={variableInputTypeToInputType(item.type)}
form={variablesForm}
fieldName={`variables.${item.key}`}
@@ -285,8 +302,9 @@ export const useDebug = () => {
))}
{filteredVar.map((item) => (
<LabelAndFormRender
{...item}
key={item.key}
label={item.label}
required={item.required}
placeholder={item.description}
inputType={variableInputTypeToInputType(item.type)}
form={variablesForm}
@@ -295,19 +313,6 @@ export const useDebug = () => {
/>
))}
</Box>
<Box display={currentTab === TabEnum.node ? 'block' : 'none'}>
{renderInputs.map((item) => (
<LabelAndFormRender
{...item}
key={item.key}
placeholder={item.placeholder || item.description}
inputType={nodeInputTypeToInputType(item.renderTypeList)}
form={variablesForm}
fieldName={`nodeVariables.${item.key}`}
bg={'myGray.50'}
/>
))}
</Box>
</Box>
<Flex py={2} justifyContent={'flex-end'} px={6}>
<Button onClick={handleSubmit(onClickRun, onCheckRunError)}>{t('common:Run')}</Button>

View File

@@ -514,7 +514,8 @@ const MenuRender = React.memo(function MenuRender({
version: template.version,
versionLabel: template.versionLabel,
isLatestVersion: template.isLatestVersion,
toolConfig: template.toolConfig
toolConfig: template.toolConfig,
catchError: template.catchError
},
selected: true,
parentNodeId: undefined,

View File

@@ -707,25 +707,31 @@ const WorkflowContextProvider = ({
try {
// 3. Run one step
const { memoryEdges, entryNodeIds, skipNodeQueue, nodeResponses, newVariables } =
await postWorkflowDebug({
nodes: runtimeNodes,
edges: debugData.runtimeEdges,
skipNodeQueue: debugData.skipNodeQueue,
variables: {
appId,
cTime: formatTime2YMDHMW(),
...debugData.variables
},
query: debugData.query, // 添加 query 参数
history: debugData.history,
const {
memoryEdges,
memoryNodes,
entryNodeIds,
skipNodeQueue,
nodeResponses,
newVariables
} = await postWorkflowDebug({
nodes: runtimeNodes,
edges: debugData.runtimeEdges,
skipNodeQueue: debugData.skipNodeQueue,
variables: {
appId,
chatConfig: appDetail.chatConfig
});
cTime: formatTime2YMDHMW(),
...debugData.variables
},
query: debugData.query, // 添加 query 参数
history: debugData.history,
appId,
chatConfig: appDetail.chatConfig
});
// 4. Store debug result
setWorkflowDebugData({
runtimeNodes: debugData.runtimeNodes,
runtimeNodes: memoryNodes,
runtimeEdges: memoryEdges,
entryNodeIds,
skipNodeQueue,
@@ -776,7 +782,7 @@ const WorkflowContextProvider = ({
console.log(error);
}
},
[appId, onChangeNode, setNodes]
[appId, onChangeNode, setNodes, appDetail.chatConfig]
);
const onStopNodeDebug = useMemoizedFn(() => {
setWorkflowDebugData(undefined);