FastGPT 工作流表单输入节点的值持久化复盘:预览页重开后表单被重置为默认值的根因与修复

FastGPT 工作流表单输入节点的值持久化复盘:预览页重开后表单被重置为默认值的根因与修复 FastGPT 工作流表单输入节点的值持久化复盘预览页重开后表单被重置为默认值的根因与修复【免费下载链接】FastGPTFastGPT is a knowledge-based platform built on the LLMs, offers a comprehensive suite of out-of-the-box capabilities such as data processing, RAG retrieval, and visual AI workflow orchestration, letting you easily develop and deploy complex question-answering systems without the need for extensive setup or configuration.项目地址: https://gitcode.com/GitHub_Trending/fa/FastGPT本文围绕 FastGPT 工作流中「表单输入userInput交互节点」的一个真实缺陷展开用户提交表单后关闭预览页面再重新打开表单内容被恢复为默认值而不是用户已填写的值。文章完整继承缺陷分析文档的复现路径、数据结构、根因定位与候选方案并结合当前仓库源码说明该问题在前端提交改写、流恢复stream resume与后端节点持久化三条链路上的最终实现读完你可以掌握 FastGPT 交互式工作流interactive的值存储模型与跨会话恢复机制。一、问题描述表单内容在重开预览页后被重置复现场景位于工作流编辑器的运行预览页面在工作流中添加「表单输入」节点在运行预览页面发起对话测试触发表单输入交互正常填写表单并提交任务继续运行成功关闭预览页面重新打开预览页面问题表单内容被恢复为默认值defaultValue而不是用户之前填写的值。该缺陷的完整分析记录见 workflow-form-input-restore-bug.md。它的本质是一个「已提交数据未写回聊天记录」的持久化缺陷表单渲染时优先读取inputForm[].valuevalue为空才回退到defaultValue而提交链路只标记了submitted: true没有把用户填写的值写进value字段于是聊天记录重新加载后所有字段都退化为默认值。二、交互数据结构表单值应该存放在哪里表单输入交互的类型定义位于 type.ts。文档中引用的核心片段在仓库中的完整形态如下UserInputFormItemSchematype.ts#L144-L172export const UserInputFormItemSchema AppFileSelectConfigTypeSchema.extend({ type: z.enum(FlowNodeInputTypeEnum), // 控件类型文本/密码/数字/下拉/文件等 key: string, // 字段键名也是提交 JSON 中的 key label: string, // 展示标签 value: z.any(), // 用户填写的值持久化真源 valueType: z.enum(WorkflowIOValueTypeEnum), // 工作流变量类型 description: z.string().optional(), defaultValue: z.any().optional(), // 设计器中配置的默认值 required: z.boolean(), maxLength: z.number().optional(), // input textarea minLength: z.number().optional(), // password max: z.number().optional(), // numberInput min: z.number().optional(), // numberInput list: z.array(z.object({ label: z.string(), value: z.string() })).optional(), // select 选项 canLocalUpload: z.boolean().optional(), // 文件选择是否允许本地上传 canUrlUpload: z.boolean().optional() // 文件选择是否允许 URL 上传 }); export const UserInputInteractiveSchema z.object({ type: z.literal(userInput), params: z.object({ description: z.string(), inputForm: z.array(UserInputFormItemSchema), submitted: z.boolean().optional() // 表单是否已提交 }) });与文档摘要相比仓库中的实际 Schema 还包含maxLength/minLength/max/min按控件类型生效的长度与数值边界、listselect 选项与文件上传开关等字段这些约束决定了前端表单控件的校验行为。设计意图是明确的value字段存储用户填写的值随聊天记录的interactive对象一起持久化submitted标记表单是否已提交用于控制是否还允许重复提交重新打开预览页时前端从聊天记录恢复interactivedefaultValues从item.value读取即可还原用户填写内容。三、根因分析3.1 defaultValues 的计算逻辑value 为空即回退默认值表单渲染组件中defaultValues的计算逻辑缺陷版本为const defaultValues useMemo(() { return interactive.params.inputForm?.reduce((acc: Recordstring, any, item) { acc[item.key] item.value ?? item.defaultValue; return acc; }, {}); }, [interactive]);逻辑本身是合理的item.value优先于item.defaultValue。问题在于当页面重新打开、interactive.params从聊天记录恢复时如果提交链路从未把用户填写的值写入item.value所有字段都会命中??分支回退到defaultValue——这正是「表单被重置」的直接表现。3.2 sessionStorage 的冗余使用只写不读缺陷版本的前端代码文档引用时位于单体文件AIResponseBox.tsx的RenderUserFormInteractive组件第 248–271 行在表单提交时执行if (typeof window ! undefined) { const dataToSave { ...data }; // ... 处理文件数据 sessionStorage.setItem(interactiveForm_${chatItemDataId}, JSON.stringify(dataToSave)); }通过全局搜索可以确认这段代码只有写入没有任何读取操作。它是一段无效代码增加了复杂度但没有实际作用。当前仓库中该写入逻辑已被移除chat 相关目录下仅保留了聊天输入框草稿对 sessionStorage 的使用useChatInputForm.ts表单交互不再依赖它。3.3 核心问题定位提交改写函数只标记了 submitted文档定位的根源在提交后的历史改写函数rewriteHistoriesByInteractiveResponse文档引用时位于ChatBox/utils.ts第 154–168 行。缺陷版本对userInput交互的处理是if ( finalInteractive.type userInput || finalInteractive.type agentPlanAskUserForm ) { return { ...val, interactive: { ...finalInteractive, params: { ...finalInteractive.params, submitted: true // 只设置了 submitted // 但没有更新 inputForm[].value } } }; }用户提交的表单数据以 JSON 字符串形式放在interactiveVal参数中函数只是简单标记submitted: true没有解析interactiveVal并回填params.inputForm[].value。其后果链条是期望流程应该是这样用户填写表单 → 提交时发送到后端 → 更新 interactive.params.inputForm[].value → 保存到聊天记录 → 关闭预览页面 → 重新打开预览页面 → 从聊天记录恢复 interactive → defaultValues 从 item.value 读取 → 表单显示用户填写的值实际流程出问题时用户填写表单 → 提交时发送到后端 → 前端改写历史时只标记 submitted: truevalue 未更新 → 关闭预览页面 → 重新打开预览页面 → 从聊天记录恢复 interactive → interactive.params.inputForm[].value 为空 → defaultValues 回退到 item.defaultValue → 表单显示默认值3.4 一个补充事实提交值最终也会写入后端历史值得说明的是表单提交值在后端侧并非完全丢失。工作流引擎处理表单提交时会把用户输入解析并输出为formInputResult见第七节随节点运行结果进入同一条 AI 消息的responseData。因此缺陷版本并非「值完全没有落库」而是交互对象interactive上的value字段没有被 hydrate——渲染层只读interactive.params.inputForm[].value于是出现了「后端有、交互对象没有」的数据不一致。这一事实直接催生了当前仓库中「渲染层兜底」与「流恢复回填」两条补偿链路第六节。四、sessionStorage 的设计意图复盘文档在深入分析后指出sessionStorage 的使用「可能有其合理性」这些场景分析对理解最终方案有参考价值。chatItemDataId 的含义chatItemDataId是每条聊天消息的唯一标识不是 chatId一个对话chatId中可能有多条消息每条消息有不同的dataId一个工作流中可能有多个表单输入节点每个节点触发时会创建新的消息。可能的场景场景 1同一对话中多个表单输入对话开始 → 触发表单输入节点 A (dataId: xxx-1) → 用户填写表单 A提交继续执行 → 触发表单输入节点 B (dataId: xxx-2) → 用户填写表单 B → 关闭预览页面重新打开 → 需要恢复两个表单的数据场景 2表单数据的临时性用户可能在填写过程中关闭页面未提交sessionStorage 可以保存未提交的草稿重新打开时恢复草稿避免用户重新填写。为什么单纯靠后端保存不够未提交的数据用户填写了一半但未提交后端没有这些数据多个表单实例同一对话中可能有多个表单输入节点需要按消息粒度分别保存临时状态表单的临时编辑状态如文件上传中不应该保存到后端。五、修复方案文档提出的两个候选方案 1双重保存机制sessionStorage interactive.params文档推荐结合两种机制的优点sessionStorage 保存未提交的草稿和临时状态interactive.params 保存已提交的最终数据。步骤 1修复rewriteHistoriesByInteractiveResponse已提交数据if ( finalInteractive.type userInput || finalInteractive.type agentPlanAskUserForm ) { // 解析用户提交的表单数据 let submittedData: Recordstring, any {}; try { submittedData JSON.parse(interactiveVal); } catch (error) { console.warn(Failed to parse form input data, error); } // 更新 inputForm 中的 value const updatedInputForm finalInteractive.params.inputForm.map((item) ({ ...item, value: submittedData[item.key] ?? item.value ?? item.defaultValue })); return { ...val, interactive: { ...finalInteractive, params: { ...finalInteractive.params, inputForm: updatedInputForm, submitted: true } } }; }步骤 2修复defaultValues计算逻辑恢复草稿const defaultValues useMemo(() { // 1. 优先从 sessionStorage 恢复数据(包括未提交的草稿) let savedData: Recordstring, any | null null; if (typeof window ! undefined) { try { const saved sessionStorage.getItem(interactiveForm_${chatItemDataId}); if (saved) { savedData JSON.parse(saved); } } catch (error) { console.warn(Failed to restore form data from sessionStorage, error); } } // 2. 优先级: sessionStorage(草稿) item.value(已提交) item.defaultValue(默认) return interactive.params.inputForm?.reduce((acc: Recordstring, any, item) { if (savedData item.key in savedData) { acc[item.key] savedData[item.key]; } else { acc[item.key] item.value ?? item.defaultValue; } return acc; }, {}); }, [interactive, chatItemDataId]);步骤 3清理 sessionStorage可选优化——在表单提交成功后sessionStorage.removeItem(interactiveForm_${chatItemDataId})。方案 1 的优点保留草稿保存能力、修复已提交数据的持久化、支持多表单场景、向后兼容缺点是需要改动两处逻辑稍复杂。方案 2仅修复 interactive.params简化方案如果不需要草稿保存功能只修复rewriteHistoriesByInteractiveResponse并删除 sessionStorage 相关代码。优点是简单清晰缺点是失去草稿保存能力。文档最终推荐方案 1理由保留 sessionStorage 的设计意图、修复持久化问题、覆盖复杂场景多表单、未提交草稿、向后兼容。六、当前仓库的最终实现聊天记录为单一真源formInputResult 做兜底从当前仓库源码看最终落地的是方案 2 的思路并做了两处增强表单交互彻底移除了 sessionStorage 依赖已提交值的唯一真源是聊天历史interactive 节点运行结果渲染层与流恢复层通过formInputResult对旧数据做兜底。相关代码已从文档引用时的单体文件重组为独立模块文档引用位置当前仓库位置AIResponseBox.tsx内RenderUserFormInteractiveRenderUserFormInteractive.tsxChatBox/utils.ts内rewriteHistoriesByInteractiveResponseinteractive.ts表单组件FormInputComponentInteractiveComponents.tsx6.1 提交链路rewriteHistoriesByInteractiveResponse 写回 value当前实现interactive.ts#L384-L410正是文档方案 1 步骤 1 的落地——解析interactiveVal并回填每个字段的valueif (finalInteractive.type userInput) { const submittedData: Recordstring, any (() { try { return JSON.parse(interactiveVal); } catch { return {}; } })(); // 更新 inputForm 中的 value。 const updatedInputForm finalInteractive.params.inputForm.map((item) ({ ...item, value: submittedData[item.key] ?? item.value })); return { ...val, interactive: { ...finalInteractive, params: { ...finalInteractive.params, inputForm: updatedInputForm, submitted: true } } }; }该函数由 useChatGenerate.ts 在用户提交交互时被调用约第 772 行是「关闭预览页再打开」场景下值能够恢复的第一道保障提交瞬间value已写进前端历史随消息持久化到后端。6.2 流恢复链路refreshSubmittedFormInteractiveValues 回填节点运行结果针对「恢复流中 interactive 上的inputForm.value可能仍为空持久化时未 hydrate」的旧数据仓库新增了 refreshSubmittedFormInteractiveValues当流恢复收到携带formInputResult的flowNodeResponse时把节点运行结果写回已提交的表单交互节点。其匹配策略有两级精确匹配交互的entryNodeIds包含nodeResponse.nodeId兜底匹配全历史中仅有一个 submitted 表单交互且其字段 key 与formInputResult有交集覆盖 dataId 漂移场景。对fileSelect字段回填时优先保留历史中持久化的原始 key/url name/typeURL 运行结果仅在原始值缺失时兜底通过resolveFormInputFileValues实现见第六节末。无任何字段更新时函数返回原histories引用避免触发多余渲染。该函数在 useChatGenerate.ts 约第 201 行被调用。6.3 渲染层兜底getInputFormValueFromResponseDataRenderUserFormInteractive.tsx#L20-L45 从同条 AI 消息的responseData中反向查找最近一条匹配的formInputResultnodeId在entryNodeIds内或未指定 nodeId 时取最近一条作为defaultValues的来源。因此当前defaultValues的优先级为fileSelectinputForm.value中持久化的原始文件信息优先responseData.formInputResult仅对旧历史兜底由 FormInputResult.tsx 中的resolveFormInputFileValues归一化其他字段responseData.formInputResult中最近一次运行值优先最后回退item.value ?? item.defaultValue。此外isUserInputInteractiveSubmitted 处理了「旧聊天记录未持久化submitted」的兼容只要submitted为真、或不是最后一条消息isLastChildfalse、或responseData中存在formInputResult且字段 key 有交集即判定已提交。这与渲染组件中「非最后一条子消息时强制submitted: true禁止重复提交历史表单」RenderUserFormInteractive.tsx#L107-L129共同防止了重开页面后历史表单可被二次提交的问题。6.4 文件值的归一化FormInputResult.tsx 承担文件类表单值的兼容工作getFilenameFromFormInputFileUrl从签名下载 URL 的filenamequery 参数解析展示用文件名path 段往往只是 token不可读normalizeFormInputResultFile兼容「纯 URL 字符串」与{ name, url }对象两种历史形态统一为{ name, url }resolveFormInputFileValues确立「首次提交时保存的 key/url name/type 是唯一真源节点生成的签名 URL 仅兜底」的原则避免短链接覆盖文件名和类型。该函数被流恢复ChatBox/utils与表单交互回填RenderUserFormInteractive两处复用是文档测试建议中「文件上传场景」能够正确恢复的关键。七、后端持久化链路dispatchFormInput 如何落地表单值前端修复保证的是交互对象的值被写回而表单值进入工作流引擎与聊天历史的入口是 formInput.ts 中的dispatchFormInputformInput.ts#L83-L161入口判定节点不是入口节点!isEntry或上一轮交互不是userInput时返回一个未提交的userInput交互等待用户填写JSON 解析用户提交内容以 JSON 字符串形式进入工作流「用户输入都内容将会以 JSON 字符串格式进入工作流可以从 query 的 text 中获取」通过chatValue2RuntimePrompt(query)取出文本后JSON.parse解析失败则记录告警并返回空对象字段级处理password类型字段执行anyValueDecrypt解密fileSelect字段经formatFileSelectRuntimeValue处理优先通过fileRegistrar.registerInputFile登记并生成modelUrl否则将url或key转换为预览 URLS3 预签名默认有效期 1 小时文件数量受maxFileAmount限制模块级上限优先默认 5 个结果输出返回值中同时携带data展开后的用户输入 formInputResult表单结果的输出键供下游节点引用rewriteHistories截去当前会话记录nodeResponse: { formInputResult: userInputVal }——这正是第六节前端兜底链路读取的数据来源。对应测试 formInput.test.ts 验证了 fileSelect 值在输出前被预签名为可访问 URL 的行为presigns key-only fileSelect values before exposing form outputs与源码实现一致。八、影响范围与回归测试清单影响文件与组件AIResponseBox/RenderUserFormInteractive.tsx——表单渲染与提交逻辑ChatBox/utils/interactive.ts——提交改写与流恢复回填formInput.ts——后端表单输入处理。影响场景所有使用表单输入节点的工作流预览页面关闭后重新打开同一对话中存在多个表单输入节点按dataId粒度各自独立保存和恢复。回归测试清单继承文档的测试建议基本场景填写表单 → 提交 → 关闭预览 → 重新打开 → 验证表单内容保持多次提交填写 → 提交 → 修改 → 再次提交 → 关闭 → 重新打开 → 验证显示最后一次提交的内容文件上传包含文件选择的表单验证文件名与文件信息正确恢复重点覆盖 key/url 真源与签名 URL 兜底两条路径必填项验证验证required、maxLength/minLength、max/min等约束逻辑不受影响多个表单同一对话中多个表单输入节点验证按entryNodeIds/dataId 各自独立匹配与恢复不串数据清空对话点击「重新开始」后验证表单数据被正确清空。九、小结这个缺陷的演进过程呈现了一个清晰的工程脉络缺陷期提交链路只标记submitted: true而不回填inputForm[].value叠加一段只写不读的 sessionStorage 死代码导致重开预览页后表单值退化为默认值方案期文档给出「双重保存sessionStorage 草稿 interactive.params 已提交值」与「仅修复 interactive.params」两个候选推荐前者以保留草稿能力落地期从当前仓库源码看最终收敛为「聊天历史单一真源」——提交时解析interactiveVal写回valueformInputResult随节点运行结果进入responseData旧数据则通过流恢复回填refreshSubmittedFormInteractiveValues与渲染层兜底getInputFormValueFromResponseData两条链路补偿sessionStorage 死代码被移除文件类字段以「原始 key/url 为真源、签名 URL 仅兜底」的原则保证恢复展示的一致性。对在其他交互式工作流平台做类似设计的读者本文有三个可迁移的结论提交回写必须发生在「改写历史」这一步而不是只改状态标志恢复优先级应当显式定义为「持久化真源 运行结果兜底 默认值」三级任何临时存储sessionStorage 之类如果只有写入没有读取路径就是在为未来的恢复逻辑埋下不一致的隐患。【免费下载链接】FastGPTFastGPT is a knowledge-based platform built on the LLMs, offers a comprehensive suite of out-of-the-box capabilities such as data processing, RAG retrieval, and visual AI workflow orchestration, letting you easily develop and deploy complex question-answering systems without the need for extensive setup or configuration.项目地址: https://gitcode.com/GitHub_Trending/fa/FastGPT创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考