FastGPT 工作流表单输入节点的值持久化复盘:预览页重开后表单被重置为默认值的根因与修复
2026/9/10 3:27:19 网站建设 项目流程

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)的值存储模型与跨会话恢复机制。

一、问题描述:表单内容在重开预览页后被重置

复现场景位于工作流编辑器的运行预览页面:

  1. 在工作流中添加「表单输入」节点;
  2. 在运行预览页面发起对话测试,触发表单输入交互;
  3. 正常填写表单并提交,任务继续运行成功;
  4. 关闭预览页面;
  5. 重新打开预览页面;
  6. 问题:表单内容被恢复为默认值(defaultValue),而不是用户之前填写的值。

该缺陷的完整分析记录见 workflow-form-input-restore-bug.md。它的本质是一个「已提交数据未写回聊天记录」的持久化缺陷:表单渲染时优先读取inputForm[].valuevalue为空才回退到defaultValue;而提交链路只标记了submitted: true,没有把用户填写的值写进value字段,于是聊天记录重新加载后所有字段都退化为默认值。

二、交互数据结构:表单值应该存放在哪里

表单输入交互的类型定义位于 type.ts。文档中引用的核心片段在仓库中的完整形态如下(UserInputFormItemSchema,type.ts#L144-L172):

export 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(按控件类型生效的长度与数值边界)、list(select 选项)与文件上传开关等字段,这些约束决定了前端表单控件的校验行为。

设计意图是明确的:

  • value字段存储用户填写的值,随聊天记录的interactive对象一起持久化;
  • submitted标记表单是否已提交,用于控制是否还允许重复提交;
  • 重新打开预览页时,前端从聊天记录恢复interactivedefaultValuesitem.value读取即可还原用户填写内容。

三、根因分析

3.1 defaultValues 的计算逻辑:value 为空即回退默认值

表单渲染组件中defaultValues的计算逻辑(缺陷版本)为:

const defaultValues = useMemo(() => { return interactive.params.inputForm?.reduce((acc: Record<string, 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.tsxRenderUserFormInteractive组件,第 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: true,value 未更新 → 关闭预览页面 → 重新打开预览页面 → 从聊天记录恢复 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. 未提交的数据:用户填写了一半但未提交,后端没有这些数据;
  2. 多个表单实例:同一对话中可能有多个表单输入节点,需要按消息粒度分别保存;
  3. 临时状态:表单的临时编辑状态(如文件上传中)不应该保存到后端。

五、修复方案:文档提出的两个候选

方案 1:双重保存机制(sessionStorage + interactive.params,文档推荐)

结合两种机制的优点:sessionStorage 保存未提交的草稿和临时状态,interactive.params 保存已提交的最终数据。

步骤 1:修复rewriteHistoriesByInteractiveResponse(已提交数据)

if ( finalInteractive.type === 'userInput' || finalInteractive.type === 'agentPlanAskUserForm' ) { // 解析用户提交的表单数据 let submittedData: Record<string, 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: Record<string, 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: Record<string, 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.tsxRenderUserFormInteractiveRenderUserFormInteractive.tsx
ChatBox/utils.tsrewriteHistoriesByInteractiveResponseinteractive.ts
表单组件FormInputComponentInteractiveComponents.tsx

6.1 提交链路:rewriteHistoriesByInteractiveResponse 写回 value

当前实现(interactive.ts#L384-L410)正是文档方案 1 步骤 1 的落地——解析interactiveVal并回填每个字段的value

if (finalInteractive.type === 'userInput') { const submittedData: Record<string, 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:当流恢复收到携带formInputResultflowNodeResponse时,把节点运行结果写回已提交的表单交互节点。其匹配策略有两级:

  1. 精确匹配:交互的entryNodeIds包含nodeResponse.nodeId
  2. 兜底匹配:全历史中仅有一个 submitted 表单交互,且其字段 key 与formInputResult有交集(覆盖 dataId 漂移场景)。

fileSelect字段,回填时优先保留历史中持久化的原始 key/url + name/type,URL 运行结果仅在原始值缺失时兜底(通过resolveFormInputFileValues实现,见第六节末)。无任何字段更新时函数返回原histories引用,避免触发多余渲染。该函数在 useChatGenerate.ts 约第 201 行被调用。

6.3 渲染层兜底:getInputFormValueFromResponseData

RenderUserFormInteractive.tsx#L20-L45 从同条 AI 消息的responseData中反向查找最近一条匹配的formInputResultnodeIdentryNodeIds内,或未指定 nodeId 时取最近一条),作为defaultValues的来源。因此当前defaultValues的优先级为:

  1. fileSelectinputForm.value中持久化的原始文件信息优先,responseData.formInputResult仅对旧历史兜底(由 FormInputResult.tsx 中的resolveFormInputFileValues归一化);
  2. 其他字段responseData.formInputResult中最近一次运行值优先;
  3. 最后回退item.value ?? item.defaultValue

此外,isUserInputInteractiveSubmitted 处理了「旧聊天记录未持久化submitted」的兼容:只要submitted为真、或不是最后一条消息(isLastChild=false)、或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 中的dispatchFormInput(formInput.ts#L83-L161):

  1. 入口判定:节点不是入口节点(!isEntry)或上一轮交互不是userInput时,返回一个未提交的userInput交互,等待用户填写;
  2. JSON 解析:用户提交内容以 JSON 字符串形式进入工作流(「用户输入都内容,将会以 JSON 字符串格式进入工作流,可以从 query 的 text 中获取」),通过chatValue2RuntimePrompt(query)取出文本后JSON.parse,解析失败则记录告警并返回空对象;
  3. 字段级处理
    • password类型字段执行anyValueDecrypt解密;
    • fileSelect字段经formatFileSelectRuntimeValue处理:优先通过fileRegistrar.registerInputFile登记并生成modelUrl,否则将urlkey转换为预览 URL(S3 预签名,默认有效期 1 小时),文件数量受maxFileAmount限制(模块级上限优先,默认 5 个);
  4. 结果输出:返回值中同时携带
    • 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粒度各自独立保存和恢复)。

回归测试清单(继承文档的测试建议):

  1. 基本场景:填写表单 → 提交 → 关闭预览 → 重新打开 → 验证表单内容保持;
  2. 多次提交:填写 → 提交 → 修改 → 再次提交 → 关闭 → 重新打开 → 验证显示最后一次提交的内容;
  3. 文件上传:包含文件选择的表单,验证文件名与文件信息正确恢复(重点覆盖 key/url 真源与签名 URL 兜底两条路径);
  4. 必填项验证:验证requiredmaxLength/minLengthmax/min等约束逻辑不受影响;
  5. 多个表单:同一对话中多个表单输入节点,验证按entryNodeIds/dataId 各自独立匹配与恢复,不串数据;
  6. 清空对话:点击「重新开始」后,验证表单数据被正确清空。

九、小结

这个缺陷的演进过程呈现了一个清晰的工程脉络:

  1. 缺陷期:提交链路只标记submitted: true而不回填inputForm[].value,叠加一段只写不读的 sessionStorage 死代码,导致重开预览页后表单值退化为默认值;
  2. 方案期:文档给出「双重保存(sessionStorage 草稿 + interactive.params 已提交值)」与「仅修复 interactive.params」两个候选,推荐前者以保留草稿能力;
  3. 落地期(从当前仓库源码看):最终收敛为「聊天历史单一真源」——提交时解析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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询