skills/labring/workflow-interactive-dev/SKILL.md
用于开发 FastGPT 工作流中的交互响应。详细说明了交互节点的架构、开发流程和需要修改的文件。
npx skillsauth add aiskillstore/marketplace workflow-interactive-devInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
FastGPT 工作流支持多种交互节点类型,允许在工作流执行过程中暂停并等待用户输入。本指南详细说明了如何开发新的交互节点。
当前系统支持以下交互节点类型:
交互节点的类型定义位于 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;
}
});
文件: 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;
文件: packages/global/core/workflow/node/constant.ts
如果不需要添加新的节点类型,则不需要修改这个文件。
export enum FlowNodeTypeEnum {
// ... 现有类型
yourNodeType = 'yourNodeType', // 新增节点类型
}
文件: 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
}
]
};
文件: 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
}
};
};
文件: packages/service/core/workflow/dispatch/constants.ts
import { dispatchYourNode } from './interactive/yourNode';
export const callbackMap: Record<FlowNodeTypeEnum, any> = {
// ... 现有节点
[FlowNodeTypeEnum.yourNodeType]: dispatchYourNode,
};
文件: 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>
);
});
文件: 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);
需要在节点注册表中添加你的节点组件(具体位置根据项目配置而定)。
文件: packages/web/i18n/zh-CN/app.json 和其他语言文件
{
"workflow": {
"your_node": "你的节点名称",
"your_node_tip": "节点功能说明",
"your_node_placeholder": "提示文本"
}
}
文件: FastGPT/packages/service/core/chat/saveChat.ts
修改 updateInteractiveChat 方法,支持新的交互
文件: FastGPT/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts
文件: FastGPT/packages/global/core/workflow/runtime/utils.ts
调整setInteractiveResultToHistories, getInteractiveByHistories 和 getLastInteractiveValue方法。
交互节点需要保持 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),
// ... 其他返回值
};
交互节点触发时,系统会保存 skipNodeQueue 以便恢复时跳过已处理的节点。
如果节点需要在工具调用中使用,设置 isTool: true。
开发完成后,请测试以下场景:
可以参考以下现有实现:
简单单选: userSelect 节点
packages/global/core/workflow/template/system/interactive/type.d.ts:48-55packages/service/core/workflow/dispatch/interactive/userSelect.tsprojects/app/src/components/core/chat/components/Interactive/InteractiveComponents.tsx:29-63复杂表单: formInput 节点
packages/global/core/workflow/template/system/interactive/type.d.ts:57-82packages/service/core/workflow/dispatch/interactive/formInput.tsprojects/app/src/components/core/chat/components/Interactive/InteractiveComponents.tsx:65-126A: 这是正常的。第一次返回交互请求,第二次处理用户输入。确保在第二次执行时设置 node.isEntry = false。
A: 检查你的节点类型是否在 isEntry 白名单中(dispatch/index.ts:1013-1018)。
A: 检查 chatValue2RuntimePrompt 的返回值,根据你的数据格式进行解析。
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',
}
development
Apple Human Interface Guidelines for content display components. Use this skill when the user asks about charts component, collection view, image view, web view, color well, image well, activity view, lockup, data visualization, content display, displaying images, rendering web content, color pickers, or presenting collections of items in Apple apps. Also use when the user says how should I display charts, what's the best way to show images, should I use a web view, how do I build a grid of items, what component shows media, or how do I present a share sheet. Cross-references: hig-foundations for color/typography/accessibility, hig-patterns for data visualization patterns, hig-components-layout for structural containers, hig-platforms for platform-specific component behavior.
tools
Automate HelpDesk tasks via Rube MCP (Composio): list tickets, manage views, use canned responses, and configure custom fields. Always search tools first for current schemas.
testing
Expert Haskell engineer specializing in advanced type systems, pure functional design, and high-reliability software. Use PROACTIVELY for type-level programming, concurrency, and architecture guidance.
tools
GraphQL gives clients exactly the data they need - no more, no less. One endpoint, typed schema, introspection. But the flexibility that makes it powerful also makes it dangerous. Without proper controls, clients can craft queries that bring down your server. This skill covers schema design, resolvers, DataLoader for N+1 prevention, federation for microservices, and client integration with Apollo/urql. Key insight: GraphQL is a contract. The schema is the API documentation. Design it carefully.