.agents/design/bug/stream-resume-form-input-file-list.md
流恢复过程中,工作流的「表单输入」节点可以恢复出交互节点本身,但已提交的表单值没有完整恢复,其中 fileSelect 文件列表最容易暴露。
典型表现:
getRecords_v2 返回的交互节点中有 interactive.params.inputForm[].value,已提交表单值可以从历史数据恢复。flowNodeResponse 事件,数据里包含:{
"moduleType": "formInput",
"formInputResult": {
"File": [
"http://localhost:3000/api/system/file/download/xxx?filename=H6%E4%BA%A7%E5%93%81%E6%A6%82%E8%BF%B0V1.5_tBF8kj.docx"
]
},
"nodeId": "j1Ifb41hX176ezmo"
}
用户期望:表单输入节点的已提交字段值恢复到「用户交互表单节点」内部;其中 fileSelect 应恢复为文件列表,而不是展示在 AI 普通回复气泡或文本区域里。
getRecords_v2 返回的交互节点中,字段值已经存在;其中文件字段是 FileSelector 可渲染的结构:
{
"interactive": {
"type": "userInput",
"params": {
"submitted": true,
"inputForm": [
{
"type": "fileSelect",
"key": "File",
"value": [
{
"key": "chat/xxx.docx",
"name": "H6产品概述V1.5.docx",
"type": "file",
"url": "http://localhost:3000/api/system/file/download/xxx"
}
]
}
]
},
"entryNodeIds": ["j1Ifb41hX176ezmo"]
}
}
flowNodeResponse.formInputResult 只包含字段结果,不是完整的表单渲染结构:
{
"formInputResult": {
"File": ["http://localhost:3000/api/system/file/download/xxx?filename=file.docx"]
}
}
因此前端不能直接把 formInputResult 当成 AI 文本渲染,也不能只展示在响应详情里;需要把它转换并回填到交互表单的 inputForm[].value。
以 fileSelect 为例,FileSelector 接收以下两类值都能渲染文件列表:
[{ name: 'file.docx', url: 'https://example.com/file.docx' }]
或:
[{ name: 'file.docx', key: 'chat/xxx/file.docx' }]
页面能看到禁用上传区域,说明:
submitted=true 已经生效。inputForm[].value;fileSelect 只是表现为传给 FileSelector 的 value 为空。恢复流的 flowNodeResponse 会进入 generatingMessage,并追加到当前 AI 记录的 responseData。但原始实现只保存节点响应详情,没有把 formInputResult.File 回填到已提交的 interactive.params.inputForm[].value。
结果:交互节点还在,但字段值仍是空数组。
流恢复结束时,后端会返回 completedChat.records,前端用它覆盖当前 chatRecords。
这个覆盖有两个风险:
inputForm.value 覆盖。dataId 不一定完全一致。只按 dataId 合并可能漏掉。因此需要在 completed 覆盖阶段保留 submitted interactive 中已经恢复出来的字段值。
即使状态层做了回填和 merge,仍可能出现中间态或边界情况:
responseData 已经有 formInputResult。interactive.params.inputForm[].value 被后续 records 替换成空。inputForm.value,导致已提交表单值仍然不显示。所以渲染表单时也应当能从同一条 AI 消息的 responseData.formInputResult 还原字段默认值。
本次修复不是四个备选方案,而是一个组合修复。最终采用:
恢复事件回填 + completed 覆盖保护 + 过期交互去重 + 渲染层兜底
四个修复点分别覆盖不同阶段的问题:
responseData.formInputResult 做最后兜底。位置:
projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsxprojects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts逻辑:
generatingMessage 收到 flowNodeResponse。nodeResponse.formInputResult 存在,调用 refreshSubmittedFormInteractiveValues。chatRecords 里寻找已提交的 userInput 交互节点。interactive.entryNodeIds.includes(nodeResponse.nodeId)。fileSelect 字段,把 formInputResult.File: string[] 转成:[
{
name: 'file.docx',
url: 'http://localhost:3000/api/system/file/download/xxx?filename=file.docx'
}
]
转换复用 normalizeFormInputResultFile,保证文件名从 filename query 或 URL path 中取。
位置:
projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts逻辑:
mergeResumeCompletedChatRecords 建立当前 AI record map。responseData。userInput 交互节点里的 inputForm.value。dataId 找到当前 record,则使用对应 current values。dataId 找不到,则从当前所有 AI records 中寻找 submitted interactive,并按交互身份匹配。交互身份匹配规则:
type 相同 &&
(usageId 相同 || entryNodeIds 数组相同)
这个逻辑覆盖 completed records 中交互节点 dataId 变化的情况。
位置:
projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts逻辑:
如果当前已经有同一个 submitted userInput 交互节点,恢复流里又来了同一个未提交 interactive,不再 append。
作用:
responseData.formInputResult 兜底恢复表单值位置:
projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsxprojects/app/src/components/core/chat/components/AIResponseBox/index.tsxprojects/app/src/components/core/chat/components/AIResponseBox/RenderUserFormInteractive.tsx调用链:
ChatItem
-> AIContentCard
-> AIResponseBox
-> RenderUserFormInteractive
-> FormInputComponent
-> InputRender
-> FileSelector
新增传参:
ChatItem 把当前 AI 消息的 chat.responseData 传给 AIContentCard。AIContentCard 传给 AIResponseBox。AIResponseBox 在渲染 userInput 时传给 RenderUserFormInteractive。RenderUserFormInteractive 生成 defaultValues 时:
responseData 里倒序查找带 formInputResult 的节点响应。nodeId 与 interactive.entryNodeIds。File。formInputResult 中存在该字段,则优先使用该字段值作为 submitted 表单的渲染默认值。formInputResult[key]。fileSelect 字段,额外把 URL 数组归一化为 FileSelector 可渲染的 { name, url }[]。作用:
即使状态层 inputForm.value 被 completed records 覆盖为空,只要同一条 AI 消息还带有 responseData.formInputResult,表单节点仍然能渲染出已提交表单值。
本 PR 不做以下事情:
formInputResult 渲染到 AI 普通文字回复气泡里。FileSelector 的基础交互行为。formInputResult 的输出结构。userInput 类型的假想表单交互类型;当前类型定义中不存在 agentPlanAskUserForm。代码改动:
projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx
chat.responseData 传入 AI 响应渲染链路。projects/app/src/components/core/chat/components/AIResponseBox/index.tsx
responseData 给表单交互组件。projects/app/src/components/core/chat/components/AIResponseBox/RenderUserFormInteractive.tsx
responseData.formInputResult 兜底恢复 submitted 表单字段值;fileSelect 字段额外归一化为文件列表渲染结构。projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts
projects/app/src/components/core/chat/components/FormInputResult.tsx
normalizeFormInputResultFile,并支持在响应详情里展示表单输入文件结果。projects/app/src/components/core/chat/components/WholeResponseModal.tsx
FormInputResult 展示 formInputResult。测试改动:
projects/app/test/components/core/chat/ChatContainer/ChatBox/utils.test.ts
dataId 变化保留等测试。projects/app/test/components/core/chat/components/FormInputResult.test.ts
projects/app/test/components/core/app/FileSelector/utils.test.ts
设计文档:
.agents/design/bug/stream-resume-form-input-file-list.md
projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts职责:
refreshSubmittedFormInteractiveValues:把 formInputResult 回填到 submitted 表单节点。mergeResumeCompletedChatRecords:completed records 覆盖时保留已恢复的 submitted interactive values。shouldAppendResumeInteractive:避免追加过期未提交交互。projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsxprojects/app/src/components/core/chat/components/AIResponseBox/index.tsxprojects/app/src/components/core/chat/components/AIResponseBox/RenderUserFormInteractive.tsx职责:
responseData 从 chat item 下传到表单交互渲染组件。responseData.formInputResult 兜底恢复表单字段值。projects/app/src/components/core/chat/components/FormInputResult.tsx职责:
normalizeFormInputResultFile。formInputResult 文件。注意:详情弹窗展示不是本 bug 的核心修复,核心修复是交互节点内已提交表单值恢复,文件列表只是 fileSelect 字段的展示结果。
projects/app/test/components/core/chat/ChatContainer/ChatBox/utils.test.tsprojects/app/test/components/core/chat/components/FormInputResult.test.tsprojects/app/test/components/core/app/FileSelector/utils.test.ts已覆盖场景:
flowNodeResponse.formInputResult.File 能回填到 submitted userInput 的 inputForm[].value。nodeId 不匹配但只有一个 submitted 表单交互节点时,可以按字段 key 兜底回填。dataId 变化时,仍能按交互身份保留字段值。FormInputResult 能从签名 URL 的 filename query 中解析文件名。FileSelector 的值清洗函数能保留可渲染 URL 字段值。局部测试命令:
source ~/.zshrc >/dev/null 2>&1; pnpm --filter @fastgpt/app test test/components/core/chat/ChatContainer/ChatBox/utils.test.ts test/components/core/chat/components/FormInputResult.test.ts test/components/core/app/FileSelector/utils.test.ts
当前结果:
Test Files 3 passed
Tests 32 passed
projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts
'error' is defined but never used
@fastgpt/app typecheck 在当前分支仍有无关类型错误,集中在:
ChatItem.tsx 的 stepId/stepTitle 类型声明缺失。ResponseTags.tsx / RenderResponseDetail.tsx 缺 chatTime 参数。WholeResponseModal.tsx 中 queryExtensionResult 类型名与当前 schema 不一致。这些不是本修复新增逻辑引入的问题。
flowNodeResponse 会进入前端 generatingMessage。formInputResult 到 submitted 表单交互节点。dataId 变化场景。responseData.formInputResult 兜底恢复表单值。