.agents/issue/workflow-form-input-restore-bug.md
在工作流中添加表单输入节点后,在运行预览页面进行对话测试时:
根据类型定义 packages/global/core/workflow/template/system/interactive/type.ts:
export type UserInputFormItemType = {
key: string;
label: string;
value: any; // 用户填写的值
defaultValue?: any; // 默认值
required: boolean;
// ...
}
export type UserInputInteractive = {
type: 'userInput';
params: {
description: string;
inputForm: UserInputFormItemType[];
submitted?: boolean; // 是否已提交
}
}
设计意图:
value 字段用于存储用户填写的值submitted 标记表单是否已提交interactive 对象中在 AIResponseBox.tsx 的 RenderUserFormInteractive 组件中(第 248-271 行):
if (typeof window !== 'undefined') {
const dataToSave = { ...data };
// ... 处理文件数据
sessionStorage.setItem(`interactiveForm_${chatItemDataId}`, JSON.stringify(dataToSave));
}
问题:
sessionStorage,但从未读取在 RenderUserFormInteractive 组件中(第 231-237 行):
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 从聊天记录中恢复,但:
value 到 interactive.params.inputForminteractive 对象item.value 为空,回退到 defaultValue正常流程(应该是这样):
用户填写表单
→ 提交时发送到后端
→ 后端更新 interactive.params.inputForm[].value
→ 后端保存到聊天记录
→ 关闭预览页面
→ 重新打开预览页面
→ 从聊天记录恢复 interactive
→ defaultValues 从 item.value 读取
→ 表单显示用户填写的值 ✅
实际流程(出问题了):
用户填写表单
→ 提交时发送到后端
→ 后端处理但可能没有更新 interactive.params.inputForm[].value
→ 或者前端没有正确更新本地的 interactive 对象
→ 关闭预览页面
→ 重新打开预览页面
→ 从聊天记录恢复 interactive
→ interactive.params.inputForm[].value 为空
→ defaultValues 回退到 item.defaultValue
→ 表单显示默认值 ❌
问题确认: 前端在表单提交后,只更新了 submitted: true,但没有更新 inputForm[].value
在 projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts 的 rewriteHistoriesByInteractiveResponse 函数中(第 154-168 行):
if (
finalInteractive.type === 'userInput' ||
finalInteractive.type === 'agentPlanAskUserForm'
) {
return {
...val,
interactive: {
...finalInteractive,
params: {
...finalInteractive.params,
submitted: true // ✅ 只设置了 submitted
// ❌ 但没有更新 inputForm[].value
}
}
};
}
分析:
interactiveVal 参数中(JSON 字符串格式)submitted: trueinteractiveVal 并更新 params.inputForm[].valueitem.value 仍然是空的,回退到 defaultValue经过深入分析,发现 sessionStorage 的使用可能有其合理性:
chatItemDataId 是每条聊天消息的唯一标识 (不是 chatId)dataId场景 1: 同一对话中多个表单输入
对话开始
→ 触发表单输入节点 A (dataId: xxx-1)
→ 用户填写表单 A
→ 提交,继续执行
→ 触发表单输入节点 B (dataId: xxx-2)
→ 用户填写表单 B
→ 关闭预览页面
→ 重新打开
→ 需要恢复两个表单的数据
场景 2: 表单数据的临时性
projects/app/src/components/core/chat/components/AIResponseBox.tsxprojects/app/src/components/core/chat/ChatContainer/ChatBox/utils.tsRenderUserFormInteractive, rewriteHistoriesByInteractiveResponse结合两种机制的优点:
rewriteHistoriesByInteractiveResponse (已提交数据)文件: projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts
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
}
}
};
}
defaultValues 计算逻辑 (恢复草稿)文件: projects/app/src/components/core/chat/components/AIResponseBox.tsx
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. 构建 defaultValues
// 优先级: sessionStorage(草稿) > item.value(已提交) > item.defaultValue(默认)
return interactive.params.inputForm?.reduce((acc: Record<string, any>, item) => {
if (savedData && item.key in savedData) {
// 优先使用 sessionStorage 中的数据(可能是未提交的草稿)
acc[item.key] = savedData[item.key];
} else {
// 否则使用 item.value(已提交的数据) 或 defaultValue
acc[item.key] = item.value ?? item.defaultValue;
}
return acc;
}, {});
}, [interactive, chatItemDataId]);
在表单提交成功后,清理对应的 sessionStorage:
const handleFormSubmit = useCallback(
(data: Record<string, any>) => {
const finalData: Record<string, any> = {};
interactive.params.inputForm?.forEach((item) => {
if (item.key in data) {
finalData[item.key] = data[item.key];
}
});
// 保存到 sessionStorage (用于页面关闭后恢复)
if (typeof window !== 'undefined') {
const dataToSave = { ...data };
// ... 处理文件数据
sessionStorage.setItem(`interactiveForm_${chatItemDataId}`, JSON.stringify(dataToSave));
}
onSendPrompt(JSON.stringify(finalData));
// 可选: 提交成功后清理 sessionStorage
// setTimeout(() => {
// sessionStorage.removeItem(`interactiveForm_${chatItemDataId}`);
// }, 1000);
},
[chatItemDataId, interactive.params.inputForm]
);
优点:
缺点:
如果不需要草稿保存功能,可以只修复 rewriteHistoriesByInteractiveResponse,删除 sessionStorage 相关代码。
优点: 简单,代码更清晰 缺点: 失去草稿保存功能
推荐方案 1,原因:
projects/app/src/components/core/chat/components/AIResponseBox.tsx - 表单渲染和提交逻辑projects/app/src/components/core/chat/components/Interactive/InteractiveComponents.tsx - 表单输入组件projects/app/src/web/core/chat/context/chatItemContext.tsx - Chat 上下文管理packages/service/core/workflow/dispatch/interactive/formInput.ts - 后端表单输入处理修复后需要测试以下场景: