交互节点开发指南
概述
FastGPT 工作流支持多种交互节点类型,允许在工作流执行过程中暂停并等待用户输入。本指南详细说明了如何开发新的交互节点。
现有交互节点类型
当前系统支持以下交互节点类型:
-
userSelect - 用户选择节点(单选)
-
formInput - 表单输入节点(多字段表单)
-
childrenInteractive - 子工作流交互
-
loopInteractive - 循环交互
-
paymentPause - 欠费暂停交互
交互节点架构
核心类型定义
交互节点的类型定义位于 packages/global/core/workflow/template/system/interactive/type.d.ts
// 基础交互结构 type InteractiveBasicType = { entryNodeIds: string[]; // 入口节点ID列表 memoryEdges: RuntimeEdgeItemType[]; // 需要记忆的边 nodeOutputs: NodeOutputItemType[]; // 节点输出 skipNodeQueue?: Array; // 跳过的节点队列 usageId?: string; // 用量记录ID };
// 具体交互节点类型 type YourInteractiveNode = InteractiveNodeType & { type: 'yourNodeType'; params: { // 节点特定参数 }; };
工作流执行机制
交互节点在工作流执行中的特殊处理(位于 packages/service/core/workflow/dispatch/index.ts:1012-1019 ):
// 部分交互节点不会自动重置 isEntry 标志(因为需要根据 isEntry 字段来判断是首次进入还是流程进入) runtimeNodes.forEach((item) => { if ( item.flowNodeType !== FlowNodeTypeEnum.userSelect && item.flowNodeType !== FlowNodeTypeEnum.formInput && item.flowNodeType !== FlowNodeTypeEnum.agent ) { item.isEntry = false; } });
开发新交互响应的步骤
步骤 1: 定义节点类型
文件: packages/global/core/workflow/template/system/interactive/type.d.ts
export type YourInputItemType = { // 定义输入项的结构 key: string; label: string; value: any; // ... 其他字段 };
type YourInteractiveNode = InteractiveNodeType & { type: 'yourNodeType'; params: { description: string; yourInputField: YourInputItemType[]; submitted?: boolean; // 可选:是否已提交 }; };
// 添加到联合类型 export type InteractiveNodeResponseType = | UserSelectInteractive | UserInputInteractive | YourInteractiveNode // 新增 | ChildrenInteractive | LoopInteractive | PaymentPauseInteractive;
步骤 2: 定义节点枚举(可选)
文件: packages/global/core/workflow/node/constant.ts
如果不需要添加新的节点类型,则不需要修改这个文件。
export enum FlowNodeTypeEnum { // ... 现有类型 yourNodeType = 'yourNodeType', // 新增节点类型 }
步骤 3: 创建节点模板(可选)
文件: packages/global/core/workflow/template/system/interactive/yourNode.ts
import { i18nT } from '../../../../../../web/i18n/utils'; import { FlowNodeTemplateTypeEnum, NodeInputKeyEnum, NodeOutputKeyEnum, WorkflowIOValueTypeEnum } from '../../../constants'; import { FlowNodeInputTypeEnum, FlowNodeOutputTypeEnum, FlowNodeTypeEnum } from '../../../node/constant'; import { type FlowNodeTemplateType } from '../../../type/node';
export const YourNode: FlowNodeTemplateType = { id: FlowNodeTypeEnum.yourNodeType, templateType: FlowNodeTemplateTypeEnum.interactive, flowNodeType: FlowNodeTypeEnum.yourNodeType, showSourceHandle: true, // 是否显示源连接点 showTargetHandle: true, // 是否显示目标连接点 avatar: 'core/workflow/template/yourNode', name: i18nT('app:workflow.your_node'), intro: i18nT('app:workflow.your_node_tip'), isTool: true, // 标记为工具节点 inputs: [ { key: NodeInputKeyEnum.description, renderTypeList: [FlowNodeInputTypeEnum.textarea], valueType: WorkflowIOValueTypeEnum.string, label: i18nT('app:workflow.node_description'), placeholder: i18nT('app:workflow.your_node_placeholder') }, { key: NodeInputKeyEnum.yourInputField, renderTypeList: [FlowNodeInputTypeEnum.custom], valueType: WorkflowIOValueTypeEnum.any, label: '', value: [] // 默认值 } ], outputs: [ { id: NodeOutputKeyEnum.yourResult, key: NodeOutputKeyEnum.yourResult, required: true, label: i18nT('workflow:your_result'), valueType: WorkflowIOValueTypeEnum.object, type: FlowNodeOutputTypeEnum.static } ] };
步骤 4: 创建节点执行逻辑或在需要处理交互逻辑的节点上增加新逻辑
文件: packages/service/core/workflow/dispatch/interactive/yourNode.ts
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import type { DispatchNodeResultType, ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import type { YourInputItemType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';
type Props = ModuleDispatchProps<{ [NodeInputKeyEnum.description]: string; [NodeInputKeyEnum.yourInputField]: YourInputItemType[]; }>;
type YourNodeResponse = DispatchNodeResultType<{ [NodeOutputKeyEnum.yourResult]?: Record<string, any>; }>;
export const dispatchYourNode = async (props: Props): Promise<YourNodeResponse> => { const { histories, node, params: { description, yourInputField }, query, lastInteractive } = props; const { isEntry } = node;
// 第一阶段:非入口节点或不是对应的交互类型,返回交互请求 if (!isEntry || lastInteractive?.type !== 'yourNodeType') { return { [DispatchNodeResponseKeyEnum.interactive]: { type: 'yourNodeType', params: { description, yourInputField } } }; }
// 第二阶段:处理用户提交的数据 node.isEntry = false; // 重要:重置入口标志
const { text } = chatValue2RuntimePrompt(query); const userInputVal = (() => { try { return JSON.parse(text); // 根据实际格式解析 } catch (error) { return {}; } })();
return { data: { [NodeOutputKeyEnum.yourResult]: userInputVal }, // 移除当前交互的历史记录(最后2条) [DispatchNodeResponseKeyEnum.rewriteHistories]: histories.slice(0, -2), [DispatchNodeResponseKeyEnum.toolResponses]: userInputVal, [DispatchNodeResponseKeyEnum.nodeResponse]: { yourResult: userInputVal } }; };
步骤 5: 注册节点回调
文件: packages/service/core/workflow/dispatch/constants.ts
import { dispatchYourNode } from './interactive/yourNode';
export const callbackMap: Record<FlowNodeTypeEnum, any> = { // ... 现有节点 [FlowNodeTypeEnum.yourNodeType]: dispatchYourNode, };
步骤 6: 创建前端渲染组件
6.1 聊天界面交互组件
文件: projects/app/src/components/core/chat/components/Interactive/InteractiveComponents.tsx
export const YourNodeComponent = React.memo(function YourNodeComponent({ interactiveParams: { description, yourInputField, submitted }, defaultValues = {}, SubmitButton }: { interactiveParams: YourInteractiveNode['params']; defaultValues?: Record<string, any>; SubmitButton: (e: { onSubmit: UseFormHandleSubmit<Record<string, any>> }) => React.JSX.Element; }) { const { handleSubmit, control } = useForm({ defaultValues });
return ( <Box> <DescriptionBox description={description} /> <Flex flexDirection={'column'} gap={3}> {yourInputField.map((input) => ( <Box key={input.key}> {/* 渲染你的输入组件 */} <Controller control={control} name={input.key} render={({ field: { onChange, value } }) => ( <YourInputComponent value={value} onChange={onChange} isDisabled={submitted} /> )} /> </Box> ))} </Flex>
{!submitted && (
<Flex justifyContent={'flex-end'} mt={4}>
<SubmitButton onSubmit={handleSubmit} />
</Flex>
)}
</Box>
); });
6.2 工作流编辑器节点组件
文件: projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeYourNode.tsx
import React, { useMemo } from 'react'; import { type NodeProps } from 'reactflow'; import { Box, Button } from '@chakra-ui/react'; import NodeCard from './render/NodeCard'; import { type FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d'; import Container from '../components/Container'; import RenderInput from './render/RenderInput'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { useTranslation } from 'next-i18next'; import { type FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d'; import { useContextSelector } from 'use-context-selector'; import IOTitle from '../components/IOTitle'; import RenderOutput from './render/RenderOutput'; import { WorkflowActionsContext } from '../../context/workflowActionsContext';
const NodeYourNode = ({ data, selected }: NodeProps<FlowNodeItemType>) => { const { t } = useTranslation(); const { nodeId, inputs, outputs } = data; const onChangeNode = useContextSelector(WorkflowActionsContext, (v) => v.onChangeNode);
const CustomComponent = useMemo( () => ({ [NodeInputKeyEnum.yourInputField]: (v: FlowNodeInputItemType) => { // 自定义渲染逻辑 return ( <Box> {/* 你的自定义UI */} </Box> ); } }), [nodeId, onChangeNode, t] );
return ( <NodeCard minW={'400px'} selected={selected} {...data}> <Container> <RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} /> </Container> <Container> <IOTitle text={t('common:Output')} /> <RenderOutput nodeId={nodeId} flowOutputList={outputs} /> </Container> </NodeCard> ); };
export default React.memo(NodeYourNode);
步骤 7: 注册节点组件
需要在节点注册表中添加你的节点组件(具体位置根据项目配置而定)。
步骤 8: 添加国际化
文件: packages/web/i18n/zh-CN/app.json 和其他语言文件
{ "workflow": { "your_node": "你的节点名称", "your_node_tip": "节点功能说明", "your_node_placeholder": "提示文本" } }
步骤9 调整保存对话记录逻辑
文件: FastGPT/packages/service/core/chat/saveChat.ts
修改 updateInteractiveChat 方法,支持新的交互
步骤10 根据历史记录获取/设置交互状态
文件: FastGPT/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts
文件: FastGPT/packages/global/core/workflow/runtime/utils.ts
调整setInteractiveResultToHistories , getInteractiveByHistories 和 getLastInteractiveValue 方法。
关键注意事项
- isEntry 标志管理
交互节点需要保持 isEntry 标志在工作流恢复时有效:
// 在 packages/service/core/workflow/dispatch/index.ts 中 // 确保你的节点类型被添加到白名单 if ( item.flowNodeType !== FlowNodeTypeEnum.userSelect && item.flowNodeType !== FlowNodeTypeEnum.formInput && item.flowNodeType !== FlowNodeTypeEnum.yourNodeType // 新增 ) { item.isEntry = false; }
- 交互响应流程
交互节点有两个执行阶段:
-
第一次执行: 返回 interactive 响应,暂停工作流
-
第二次执行: 接收用户输入,继续工作流
// 第一阶段 if (!isEntry || lastInteractive?.type !== 'yourNodeType') { return { [DispatchNodeResponseKeyEnum.interactive]: { type: 'yourNodeType', params: { /* ... */ } } }; }
// 第二阶段 node.isEntry = false; // 重要!重置标志 // 处理用户输入...
- 历史记录管理
交互节点需要正确处理历史记录:
return { // 移除交互对话的历史记录(用户问题 + 系统响应) [DispatchNodeResponseKeyEnum.rewriteHistories]: histories.slice(0, -2), // ... 其他返回值 };
- Skip 节点队列
交互节点触发时,系统会保存 skipNodeQueue 以便恢复时跳过已处理的节点。
- 工具调用支持
如果节点需要在工具调用中使用,设置 isTool: true 。
测试清单
开发完成后,请测试以下场景:
-
节点在工作流编辑器中正常显示
-
节点配置保存和加载正确
-
交互请求正确发送到前端
-
前端组件正确渲染交互界面
-
用户输入正确传回后端
-
工作流正确恢复并继续执行
-
历史记录正确更新
-
节点输出正确连接到后续节点
-
错误情况处理正确
-
多语言支持完整
参考实现
可以参考以下现有实现:
简单单选: userSelect 节点
-
类型定义: packages/global/core/workflow/template/system/interactive/type.d.ts:48-55
-
执行逻辑: packages/service/core/workflow/dispatch/interactive/userSelect.ts
-
前端组件: projects/app/src/components/core/chat/components/Interactive/InteractiveComponents.tsx:29-63
复杂表单: formInput 节点
-
类型定义: packages/global/core/workflow/template/system/interactive/type.d.ts:57-82
-
执行逻辑: packages/service/core/workflow/dispatch/interactive/formInput.ts
-
前端组件: projects/app/src/components/core/chat/components/Interactive/InteractiveComponents.tsx:65-126
常见问题
Q: 交互节点执行了两次?
A: 这是正常的。第一次返回交互请求,第二次处理用户输入。确保在第二次执行时设置 node.isEntry = false 。
Q: 工作流恢复后没有继续执行?
A: 检查你的节点类型是否在 isEntry 白名单中(dispatch/index.ts:1013-1018)。
Q: 用户输入格式不对?
A: 检查 chatValue2RuntimePrompt 的返回值,根据你的数据格式进行解析。
Q: 如何支持多个交互节点串联?
A: 每个交互节点都会暂停工作流,用户完成后会自动继续到下一个节点。
文件清单总结
开发新交互节点需要修改/创建以下文件:
后端核心文件
-
packages/global/core/workflow/template/system/interactive/type.d.ts
-
类型定义
-
packages/global/core/workflow/node/constant.ts
-
节点枚举
-
packages/global/core/workflow/template/system/interactive/yourNode.ts
-
节点模板
-
packages/service/core/workflow/dispatch/interactive/yourNode.ts
-
执行逻辑
-
packages/service/core/workflow/dispatch/constants.ts
-
回调注册
-
packages/service/core/workflow/dispatch/index.ts
-
isEntry 白名单
前端组件文件
-
projects/app/src/components/core/chat/components/Interactive/InteractiveComponents.tsx
-
聊天交互组件
-
projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeYourNode.tsx
-
工作流编辑器组件
国际化文件
-
packages/web/i18n/zh-CN/app.json
-
中文翻译
-
packages/web/i18n/en/app.json
-
英文翻译
-
packages/web/i18n/zh-Hant/app.json
-
繁体中文翻译
附录:关键输入输出键定义
如果需要新的输入输出键,在以下文件中定义:
文件: packages/global/core/workflow/constants.ts
export enum NodeInputKeyEnum { // ... 现有键 yourInputKey = 'yourInputKey', }
export enum NodeOutputKeyEnum { // ... 现有键 yourOutputKey = 'yourOutputKey', }