.agents/design/core/chat/chatbox-refactor.md
projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx 是当前主聊天界面的核心组件,文件约 2000 行。它不是单纯的 UI 文件,而是同时承担了输入表单、滚动控制、流式生成、恢复生成、消息操作、反馈标注、记录渲染、home/chat/log 多模式分支等职责。
这种结构带来的主要问题:
chatRecords、chatBoxData、ChatBoxContext、WorkflowRuntimeContext 等多层状态。useMemo 形式存在,纯逻辑与 React 副作用混在一起,不利于补充单元测试。本次重构的目标不是重写聊天协议,也不是一次性替换状态管理,而是按职责逐步拆分 ChatBox,让后续维护能在更小的模块里完成。
ChatBox 对外调用方式不变,包括默认导出、props、ChatBoxRef.restartChat、ChatBoxRef.scrollToBottom。index.tsx 中的核心职责拆成可理解、可测试、可迭代的模块。ChatBox 外部 props 结构。ChatItemContext、ChatRecordContext、ChatBoxContext、WorkflowRuntimeContext。HelperBot,除非抽出的类型或纯工具函数天然复用。当前 ChatBox 内部维护以下 UI 或运行状态:
isLoading:重试删除等操作的加载态。feedbackId:用户点踩后打开反馈弹窗的目标消息。adminMarkData:管理员标注弹窗的数据。questionGuides:一次回答结束后生成的问题引导。expandedDeletedGroups:log 模式中被删除消息组的展开状态。这些状态分属不同领域,后续可以分别进入 feedback、question guide、record list 等模块。
ChatBox 同时读取多个 context:
ChatItemContext
chatBoxDatasetChatBoxDataChatBoxRefvariablesFormresetVariablessetIsVariableVisibleChatRecordContext
chatRecordssetChatRecordsisLoadingRecordsisChatRecordsLoadedScrollDataitemRefsWorkflowRuntimeContext
appIdchatIdoutLinkAuthDataChatBoxContext
welcomeText、variableList、questionGuidestartSegmentedAudio、splitText2Audio、finishSegmentedAudioisChattingChatContext
setHistoriesloadHistories其中 sendPrompt 和 resume 同时改写多个 context,是当前耦合最重的两块。
当前输入表单职责包括:
useForm<ChatBoxInputFormType> 初始化。sessionStorage 读取 chatInput_${chatId} 草稿。input 变化并 debounce 写入草稿。chatStarted。resetInputVal 同时重置 files、input、textarea 高度和草稿。这部分适合拆成 useChatInputForm,由它返回:
type UseChatInputFormResult = {
chatForm: UseFormReturn<ChatBoxInputFormType>;
chatStarted: boolean;
chatStartedWatch: boolean;
commonVariableList: VariableItemType[];
showExternalVariable: boolean;
resetInputVal: (value: ChatBoxInputType) => void;
};
当前滚动职责包括:
ScrollContainerRef。scrollToBottom,支持延迟和递归等待 DOM。generatingScroll,根据当前位置决定流式生成时是否跟随底部。ChatBoxRef.scrollToBottom 对外暴露。这部分适合拆成 useChatScroll,但变量可见性检测可以单独拆成较小的 useVariableInputVisibility,避免滚动 hook 承担太多 UI 观察职责。
流式生成是当前最核心、风险最高的逻辑,包含:
generatingMessage
sendPrompt
chatRecords。onStartChat。chatBoxData.chatGenerateStatus。abortRequest
这部分不应直接一次性抽到一个大 hook。建议先拆纯函数,再拆 useChatGenerate。
当前恢复生成逻辑由 enableAutoResume 相关 effect 承担,包含:
appId/chatId 重复恢复。dataId。streamResumeFetch。generatingMessage。completedChat 覆盖本地记录。resumeUnavailable。chatBoxData、侧边栏 histories、已读状态。这部分适合拆成 useChatResume,但前提是 generatingMessage、placeholder 纯逻辑、状态同步函数已经有清晰输入输出。
当前 createQuestionGuide 在回答完成后触发,职责包括:
questionGuide.open。AbortController。postQuestionGuide。questionGuides。这部分边界相对清晰,适合拆成 useQuestionGuide。
当前消息操作包括:
onDelMessage:支持外部自定义删除,否则调用默认 API。retryInput:删除当前消息及后续消息,然后按旧输入重发。delOneMessage:删除一条 human 消息及紧随其后的 AI 回复。这部分适合拆成 useChatRecordActions。它依赖 chatRecords、setChatRecords、sendPrompt、删除 API 和 toast。
当前反馈与标注职责包括:
onMarkSelectMarkCollectionupdateChatAdminFeedbackonAddUserLikeonAddUserDislikeFeedbackModalupdateChatUserFeedbackonCloseCustomFeedbackcloseCustomFeedbackonToggleFeedbackReadStatusupdateFeedbackReadStatusonTriggerRefresh这部分适合拆成 useChatFeedbackActions 和 ChatBoxModals。其中 hook 负责动作,modal 组件负责弹窗渲染。
当前记录渲染职责包括:
expandedDeletedGroups 决定是否渲染删除消息。ChatItem。ChatItem。这部分 JSX 大、参数多,但领域相对明确,适合拆成 ChatRecordsList。分组逻辑应先抽成纯函数,避免新组件内部继续塞复杂计算。
当前 ChatBox 根据 isHomeRender 分成两套渲染:
WelcomeHomeBoxQuickAppsChatHomeVariablesFormChatInputVariableInputFormChatInput这部分适合在 UI 拆分阶段拆成 HomeChatMain、AppChatMain、ChatInputArea。
chatRecords
ChatRecordContext 提供。chatBoxData
appId、chatId、app、userAvatar、chatGenerateStatus、hasBeenRead 等。ChatItemContext 提供。appId/chatId/outLinkAuthData
WorkflowRuntimeContext 提供。ChatProvider 从 chatBoxData.app.chatConfig 派生到 ChatBoxContext。ChatInput / eventBus / autoExecute
-> sendPrompt
-> variablesForm.handleSubmit
-> format requestVariables
-> create human item + AI placeholder
-> setChatRecords
-> setChatBoxData(generating)
-> syncSidebarChatGenerateStatus(generating)
-> onStartChat(generatingMessage)
-> generatingMessage updates last AI item
-> finish/error
-> setChatRecords(finish/error)
-> setChatBoxData(done/error)
-> syncSidebarChatGenerateStatus(done/error)
-> postMarkChatRead when active chat
-> createQuestionGuide when no interactive
chatBoxData.chatGenerateStatus === generating
+ enableAutoResume
+ records loaded
+ appId/chatId matched
-> streamResumeFetch
-> upsert AI placeholder when needed
-> generatingMessage updates last AI item
-> completedChat replaces records or local item finish
-> setChatBoxData(done/error/generating)
-> syncSidebarChatGenerateStatus
-> postMarkChatRead when active chat
chatRecords
-> log mode deleted group preprocessing
-> RecordsBox
-> ChatItem
-> feedback/mark/delete/retry callbacks
Provider.tsx 目前负责配置派生和音频能力,暂不优先改动。最终目标结构可以演进为:
ChatBox/
index.tsx
Provider.tsx
type.ts
constants.ts
utils.ts
scrollUtils.ts
hooks/
useChatBox.tsx
useChatInputForm.ts
useChatScroll.ts
useVariableInputVisibility.ts
useChatGenerate.ts
useChatResume.ts
useQuestionGuide.ts
useChatRecordActions.ts
useChatFeedbackActions.ts
components/
ChatRecordsList.tsx
AppChatMain.tsx
HomeChatMain.tsx
ChatInputArea.tsx
ChatBoxModals.tsx
utils/
generateMessage.ts
recordGroups.ts
requestVariables.ts
resume.ts
说明:
utils.ts、scrollUtils.ts 可以继续保留;如果文件职责变多,再考虑迁移到 utils/ 子目录。hooks/useChatBox.tsx 当前已经存在,后续需要判断它是否仍然只是导出 context hook,避免命名冲突。PR 1 的目标是只移动纯计算逻辑,不改变 React 生命周期、context 数据流、UI 结构和请求副作用。这样第一步可以先降低 ChatBox/index.tsx 的认知负担,同时为后续 hook/UI 拆分建立可测试的基础工具函数。
本 PR 抽出了 3 个纯逻辑文件。
recordGroups.ts从 ChatBox/index.tsx 中抽离的是 log 模式下的删除消息分组逻辑,原来对应 processedRecords 里的大段 useMemoEnhance 计算。
它负责:
ChatTypeEnum.log。chatRecords 中连续带 deleteTime 的消息。collapseTop。collapseBottom。expandedDeletedGroups 判断整组是否展开。输入:
{
chatType: ChatTypeEnum;
chatRecords: ChatSiteItemType[];
expandedDeletedGroups: Set<string>;
}
输出:
ChatSiteItemType[]
关键边界:
chatRecords 引用,不制造额外对象变化。collapseTop/collapseBottom 写回原始 records。collapseTop 和 collapseBottom。dataId 都在 expandedDeletedGroups 中时,整组才算展开。作用:
ChatBox/index.tsx 不再承担 deleted records 分组细节。ChatRecordsList 时,可以直接复用这个纯函数。requestVariables.ts从 ChatBox/index.tsx 中抽离的是 sendPrompt 里将变量表单值转换为请求 variables 的逻辑。
它负责:
variableList 中声明过的变量 key。null、undefined 使用变量配置里的 defaultValue。timePointSelect 做时间点格式化。timeRangeSelect 做时间范围格式化。valueTypeFormat 将值转换成 workflow runtime 期望的类型。输入:
{
variableList?: VariableItemType[];
variables?: Record<string, any>;
}
输出:
Record<string, any>
关键边界:
YYYY-MM-DD HH:mm:ss,再走 valueTypeFormat。timeRangeSelect 里的空字符串会保留为空字符串,用来表达未选择的边界。variableList 为空时返回空对象。作用:
sendPrompt 中的变量清洗逻辑独立出来,减少发送流程内的分支。useChatGenerate 降低复杂度。resume.ts从 ChatBox/index.tsx 中抽离的是恢复生成相关的两个纯判断:
shouldCreateResumeAiPlaceholderhasMeaningfulAiOutputshouldCreateResumeAiPlaceholder 负责判断恢复流中遇到某个 SSE event 时,是否需要提前创建 AI placeholder。
需要创建 placeholder 的事件包括:
flowNodeResponseflowNodeStatusanswerfastAnswertoolCalltoolParamstoolResponseinteractiveplanplanStatusworkflowDuration不创建 placeholder 的典型事件:
error:只影响最终状态和 toast,不应该制造空 AI 气泡。updateVariables:只回写变量,不代表有可展示的 AI 输出。hasMeaningfulAiOutput 负责判断恢复生成结束或失败后,一个 AI placeholder 是否已经有值得保留的输出。
会被认为“有意义”的内容包括:
responseDatatext.contentreasoning.contentparams/responseparams/responseskillsplaninteractive关键边界:
responseData 即使没有文本,也要保留,因为它承载节点响应详情。skills/plan/interactive 本身就是可见 UI 块,不要求额外文本。作用:
useChatResume 时可以复用这些判断,不再把规则藏在 effect 里。generatingMessage设计文档原计划评估是否将 generatingMessage 拆成 reducer。评估后决定不放进 PR 1。
原因:
generatingMessage 虽然包含大量可计算分支,但它并不是完全纯逻辑。setChatRecords,且依赖“只更新最后一条 AI 消息”的状态约束。resetVariables 等副作用。因此,PR 1 只处理已经明确纯净且可独立验证的逻辑;generatingMessage 放到阶段三,和 useChatGenerate 一起拆。
PR 2 的目标是抽离低风险 React 副作用和基础状态管理,不触碰发送协议、SSE 处理、恢复生成、消息操作和 UI 拆分。这样可以让 ChatBox/index.tsx 先少承担输入、滚动、变量可见性、问题引导这几类基础职责,同时为后续 useChatGenerate、useChatResume 提供更清晰的依赖边界。
本 PR 抽出了 4 个 hook。
hooks/useChatInputForm.ts从 ChatBox/index.tsx 中抽离的是输入表单生命周期,原来对应 useForm 初始化、草稿缓存、chatStarted 计算和 resetInputVal。
它负责:
ChatBoxInputFormType 表单。sessionStorage 读取 chatInput_${chatId} 草稿作为默认输入。input,同步写入或删除草稿。chatStarted。resetInputVal,用于发送完成、编辑问题、异常恢复时重置输入文本、文件列表、草稿和 textarea 高度。关键边界:
chatStarted 仍要求 chatBoxAppId === appId,避免 app 切换过程复用旧状态。resetInputVal 会清理当前 chatId 的草稿,并在 textarea DOM 存在时恢复高度。作用:
sendPrompt 只需要消费 chatForm、chatStarted 和 resetInputVal。hooks/useChatScroll.ts从 ChatBox/index.tsx 中抽离的是滚动容器和生成中跟随底部逻辑。
它负责:
ScrollContainerRef。scrollToBottom,支持滚动行为和延迟参数。generatingScroll,在流式生成过程中根据当前位置决定是否跟随底部。关键边界:
scrollToBottom 保留原有“先延迟、再检查 DOM、未就绪再重试”的行为。generatingScroll 仍复用 shouldFollowGeneratingScroll,只有用户本来接近底部或调用方强制滚动时才滚动。index.tsx 中的 effect 和 shouldForceScrollAfterRecordsLoaded 判断;PR 2 只把实际滚动能力交给 hook,避免一次移动太多生命周期逻辑。作用:
ChatBoxRef.scrollToBottom 和生成中滚动调用方的使用方式稳定。useChatResume、useChatGenerate 时复用滚动能力做准备。hooks/useVariableInputVisibility.ts从 ChatBox/index.tsx 中抽离的是变量输入区可见性监听。
它负责:
#variable-input。getBoundingClientRect 判断是否可见。setIsVariableVisible。关键边界:
useChatScroll,避免滚动能力 hook 额外承担 ChatItemContext 状态同步。作用:
hooks/useQuestionGuide.ts从 ChatBox/index.tsx 中抽离的是回答完成后的问题引导请求。
它负责:
questionGuide.open。AbortController。postQuestionGuide。questionGuides 并延迟滚动到底部。关键边界:
chatController 和 questionGuideController 都可能阻止请求,分别对应聊天主请求已停止、问题引导请求本身已停止。作用:
createQuestionGuide。useChatGenerate 时可以把它作为外部依赖传入,而不是把请求细节混进生成 hook。PR 3 原计划覆盖 syncSidebarChatGenerateStatus、useChatGenerate 和 useChatResume。实际拆分时评估后决定先完成“侧边栏状态同步 + 恢复生成”这两个边界,不在同一个 PR 中继续搬迁普通发送链路。
原因:
sendPrompt 和 generatingMessage 同时承担普通发送、恢复生成、TTS、滚动、变量回写、interactive、tool/plan 合并等职责。useChatResume 已经依赖 generatingMessage,如果同一 PR 同时迁移两者,review 时很难判断行为是否只是移动。本 PR 抽出了 2 个 hook。
hooks/useSidebarChatGenerateStatus.ts从 ChatBox/index.tsx 中抽离的是侧边栏历史列表生成状态同步逻辑,原来对应 syncSidebarChatGenerateStatus。
它负责:
appId/chatId 或显式传入的 targetAppId/targetChatId 定位历史会话。chatGenerateStatus。hasBeenRead。updateTime。loadHistories 触发后续服务端校准。关键边界:
hasBeenRead 未显式传入时,generating 默认未读,其它状态默认已读。chatBoxData。作用:
useChatGenerate 时的依赖数量。hooks/useChatResume.ts从 ChatBox/index.tsx 中抽离的是 auto resume effect 和恢复生成的本地状态维护。
它负责:
resumeControllerRef,让页面切换或停止时可以中断恢复流。streamResumeFetch 接收恢复流。generatingMessage 复用普通发送的 SSE 增量合并逻辑。completedChat 覆盖本地 records。resumeUnavailable 占位状态。chatBoxData、侧边栏生成状态和已读状态。关键边界:
enableAutoResume 开启、records 已加载、当前 ChatBox app/chat 和 runtime app/chat 对齐,并且状态仍为 generating 时才恢复。resumedChatTargetRef 继续防止同一个 app/chat 重复恢复。activeAppIdRef/activeChatIdRef 继续防止恢复流异步结果写入已切走的会话。generatingMessage 暂时留在 index.tsx,恢复生成 hook 只消费它,确保 PR 3 不同时改动普通发送的 SSE 合并逻辑。作用:
index.tsx 不再直接承载长恢复生成 effect。useChatGenerate 留出更清晰的接口:普通发送可以继续复用 syncSidebarChatGenerateStatus 和恢复生成留下的 controller 边界。useChatGenerateuseChatGenerate 仍然是阶段三目标,但不放进本 PR。
原因:
sendPrompt 内部同时处理输入校验、变量格式化、human/AI placeholder、onStartChat、错误恢复、TTS、已读状态和侧边栏同步。generatingMessage 内部同时处理 answer、reasoning、tool、plan、interactive、sandbox、workflowDuration 和 updateVariables。因此,PR 3 先完成恢复生成和侧边栏同步;useChatGenerate 建议作为阶段三的下一次独立 PR。
PR 4 继续完成阶段三中剩余的普通生成链路,把 generatingMessage、sendPrompt 和 abortRequest 从 ChatBox/index.tsx 移入 hooks/useChatGenerate.ts。本 PR 仍然不处理消息删除、重试、反馈、标注和 UI 组件拆分,这些保留给后续阶段。
本 PR 抽出了 1 个 hook。
hooks/useChatGenerate.ts从 ChatBox/index.tsx 中抽离的是普通发送和 SSE 增量生成逻辑。
它负责:
generatingMessage
sendPrompt
onStartChat。abortRequest
关键边界:
generatingMessage 仍只更新最后一条 AI 消息;恢复生成 hook 继续复用它。rewriteHistoriesByInteractiveResponse 回写上一轮交互,不创建新的普通轮次。responseText 时,继续恢复用户输入并移除本轮 human/AI placeholder。eventBus、window message、auto execute effect 仍留在 index.tsx,只调用 hook 返回的 sendPrompt,避免本 PR 同时迁移入口监听生命周期。index.tsx 创建并传入,保证 stop/page leave/resume 继续共用同一组 abort controller。作用:
ChatBox/index.tsx 不再承载普通生成的长流程和 SSE event 分支。index.tsxeventBus、window message 和 auto execute 都是 ChatBox 的入口编排逻辑。它们依赖 lastInteractive、canSendPrompt、active、isReady、chatStarted、records loaded 状态等 UI 层条件。
本 PR 只抽生成执行能力,不同时迁移入口监听,原因:
index.tsx 可以让 review 更容易确认外部触发行为没有变化。useChatInputEvents 或类似 hook。PR 5 开始阶段四,但只处理消息记录动作,不把反馈、标注、log read status 一起移入同一个 PR。这样本 PR 的 review 可以集中确认删除与重试行为是否保持一致,下一 PR 再单独检查反馈动作。
本 PR 抽出了 1 个 hook。
hooks/useChatRecordActions.ts从 ChatBox/index.tsx 中抽离的是会直接修改聊天记录的删除与重试逻辑。
它负责:
onDelMessage
onDeleteChatItem,优先使用外部删除能力。delChatRecordById。delFile=false,避免旧输入文件在重试前被删掉。appId、chatId 和 outLinkAuthData。retryInput
dataId 找到需要重试的记录位置。chatRecords 裁剪到目标记录之前。ChatBoxInputType。sendPrompt 重新发送,并把裁剪后的 records 作为 history 传入。Retry failed,保持原有反馈方式。delOneMessage
dataId 的 AI 回复,则同步删除该 AI 回复。关键边界:
useChatGenerate 返回的 sendPrompt,不在本 hook 内处理 SSE、placeholder、TTS、问题引导和侧边栏状态。isRecordActionLoading 只表示 record action 的加载态,目前主要覆盖重试过程,不等同于聊天生成状态。作用:
ChatBox/index.tsx 不再直接承载删除和重试的副作用细节。ChatRecordsList 组件提取准备更稳定的 actions 入参。反馈动作虽然也挂在 ChatItem 上,但它和删除/重试的业务边界不同:
userGoodFeedback、userBadFeedback、customFeedbacks、adminFeedback、readFeedbackTmbIdList。FeedbackModal、SelectMarkCollection 和 onTriggerRefresh,如果和删除/重试一起移动,单个 diff 会同时覆盖 records、API、modal 三类行为。因此 PR 5 只完成 useChatRecordActions;useChatFeedbackActions 会作为阶段四的下一次独立 PR。
PR 6 继续阶段四,只处理反馈、标注和 log 模式反馈已读状态。它不拆 FeedbackModal、SelectMarkCollection 或 records list JSX,避免同时进入 UI 组件提取阶段。
本 PR 抽出了 1 个 hook。
hooks/useChatFeedbackActions.ts从 ChatBox/index.tsx 中抽离的是挂在 AI 消息上的反馈与标注动作。
它负责:
feedbackId。FeedbackModal。FeedbackModal 提交成功后,把返回内容写入目标消息的 userBadFeedback,并关闭 modal。adminMarkData。adminFeedback,进入编辑态并带回 dataset、collection、feedback data。adminFeedback,用上一条 human 文本作为 q,用当前 AI 文本作为 a。SelectMarkCollection 提交成功后调用 updateChatAdminFeedback,并把 adminFeedback 写回本地记录。feedbackType=user、AI 消息、且当前没有点踩时可用。userGoodFeedback='yes'。userGoodFeedback。feedbackType=user、AI 消息、且当前没有点赞时可用。FeedbackModal。userBadFeedback,并同步服务端。closeCustomFeedback 后,本地按 index 过滤 customFeedbacks。chatType=log 且目标消息是 AI 时可用。updateFeedbackReadStatus 成功后更新本地 isFeedbackRead。onTriggerRefresh,让外层日志统计刷新。关键边界:
ChatBox/index.tsx 仍负责渲染 modal。useChatRecordActions。SelectMarkCollection 内部在 onSuccess 后调用 onClose。作用:
ChatBox/index.tsx 不再直接承载 feedback API 调用和乐观更新细节。ChatRecordsList 和 ChatBoxModals UI 提取降低参数复杂度。index.tsxFeedbackModal 和 SelectMarkCollection 是 UI 组件拆分阶段的工作,不在 PR 6 里移动。
原因:
SelectMarkCollection 内部还有 dataset、collection、input data 三段式流程,适合在 ChatBoxModals 提取时单独 review。onMark、onAddUserLike、onAddUserDislike、onCloseCustomFeedback、onToggleFeedbackReadStatus,先稳定 actions 返回值,再拆渲染组件更稳。PR 7 开始阶段五 UI 组件提取,但只移动 ChatBox 底部弹窗层,不拆 records list、普通聊天主区域、home 主区域或输入区。这样本 PR 可以聚焦确认弹窗渲染和反馈 hook 的连接是否保持一致。
本 PR 抽出了 1 个组件。
components/ChatBoxModals.tsx从 ChatBox/index.tsx 中抽离的是用户反馈弹窗和管理员标注弹窗的条件渲染。
它负责:
FeedbackModal
feedbackId、appId、chatId 都存在时渲染。appId、chatId、feedbackId 传给 modal。SelectMarkCollection
adminMarkData 存在时渲染。adminMarkData 传给原组件。setAdminMarkData 更新时补回当前 dataId,保证 dataset/collection/input data 多步流程中不会丢失被标注消息 id。关键边界:
chatRecords。useChatFeedbackActions 管理。FeedbackModal 和 SelectMarkCollection 本身不在本 PR 内改动。index.tsx,避免本 PR 同时迁移大量 ChatItem props。作用:
ChatBox/index.tsx 的底部 modal JSX 变成单一组件调用。ChatRecordsList 减少主组件里的渲染噪音。ChatRecordsListChatRecordsList 会移动当前最大的 JSX 片段,也会一次性承接 avatar、statusBoxData、questionGuides、deleted group、反馈动作、删除重试动作、custom feedback、admin mark 展示等大量 props。
PR 7 先拆 ChatBoxModals 的原因:
useChatFeedbackActions 稳定了状态和回调边界。ChatItem action 注入。PR 8 继续阶段五 UI 组件提取,只移动聊天记录列表渲染,不拆普通聊天主区域、home 主区域和输入区。
本 PR 抽出了 1 个组件。
components/ChatRecordsList.tsx从 ChatBox/index.tsx 中抽离的是 RecordsBox 里的 records map 渲染逻辑。
它负责:
expandedDeletedGroups 判断 deleted record 是否渲染。collapseTop 和底部 collapseBottom 折叠按钮。onToggleDeletedGroup。itemRefs.current.set(item.dataId, element)。itemRefs 的事实源仍在 ChatRecordContext,本组件不创建新的 ref 容器。TimeBox 的规则。shouldShowTimeDivider 小函数里,便于后续测试或继续拆纯函数。ChatItem。onRetry 和 onDelete。hideInUI 的 human 消息。ChatItem。showVoiceIcon、statusBoxData、questionGuides、admin mark、点赞、点踩、反馈已读动作。关键边界:
getProcessedChatRecords 处理过的 records,不负责分组计算。ChatBox/index.tsx 仍负责 scroll 容器、welcome、变量表单、输入框和 home/app 分支。AppChatMain、HomeChatMain 或 ChatInputArea。作用:
index.tsx 移出,让主组件更接近编排层。AppChatMain 降低复杂度:普通聊天区域可以直接组合 welcome、变量表单和 ChatRecordsList。AppChatMainAppChatMain 会继续移动 scroll 容器、welcome、变量表单和 records list,涉及 ScrollData、ScrollContainerRef、chatStarted、chatForm、chatType 等父级编排依赖。
PR 8 先只拆 ChatRecordsList 的原因:
index.tsx 复杂度。AppChatMain 可以在 records list 稳定后作为独立 PR 处理。PR 9 继续阶段五 UI 组件提取,只移动非 home 模式下的主聊天滚动内容区,不拆 home 主区域和底部输入区。
本 PR 抽出了 1 个组件。
components/AppChatMain.tsx从 ChatBox/index.tsx 中抽离的是原 AppChatRenderBox 对应的 JSX。
它负责:
ChatRecordContext 提供的 ScrollData。ScrollContainerRef。flex、h、w、overflow、px、pb 样式。Box maxW={['100%', '92%']} h="100%" mx="auto"。welcomeText 时继续渲染 WelcomeBox。VariableInputForm,并传入 chatStarted、chatForm、chatType。ChatRecordsList。AppChatMain 通过 recordsListProps 接收上层组装好的 records list 参数。ChatBox/index.tsx 的 hooks 管理,组件只做布局组合。关键边界:
ChatInput、停止按钮和 workorder。chatRecords。作用:
ChatBox/index.tsx 的非 home 主内容区从内联 useMemo 变成组件调用。ChatInputArea 和 HomeChatMain 留出边界。index.tsx 更接近编排层:准备数据和 actions,然后组合主区域、输入区、modals。index.tsx底部输入区虽然也属于非 home 分支,但它和发送/停止运行时关系更紧:
sendPrompt、abortRequest、lastInteractive、resetInputVal、TextareaDom 和 chatForm。showWorkorder 和 canSendPrompt 条件。AppChatMain 同时拆,会让本 PR 同时移动内容区和输入区,review 范围变大。因此 PR 9 只拆主内容区;ChatInputArea 可作为后续独立 PR。
目标:不改变 UI 和 React 生命周期,先把可测试的计算逻辑移出 index.tsx。
建议提取:
recordGroups.ts
chatType、chatRecords、expandedDeletedGroups。collapseTop/collapseBottom 的 records。generateMessage.ts
setChatRecords、TTS、scroll。requestVariables.ts
valueTypeFormat。resume.ts
shouldCreateResumeAiPlaceholderhasMeaningfulAiOutput阶段一完成标准:
index.tsx 中对应内联逻辑减少。utils.test.ts、scrollUtils.test.ts 保持通过。目标:先拆低风险 hook,让主组件的基础状态更清楚。
建议顺序:
useChatInputForm
useForm、草稿、chatStarted、resetInputVal。chatId 切换时草稿 key 和 textarea 高度。useChatScroll
ScrollContainerRef、scrollToBottom、generatingScroll。useVariableInputVisibility
useChatScroll 造成职责变宽。useQuestionGuide
questionGuides。阶段二完成标准:
ChatBox 保持同样 props/ref 行为。目标:拆出最复杂的运行时逻辑,但要分小步做。
建议顺序:
syncSidebarChatGenerateStatus 到 hook 或工具函数。
appId、chatId、chatBoxData.title、setHistories、loadHistories、t。useChatGenerate
onStartChat、chatRecords、setChatRecords、变量配置、输入 reset、滚动、TTS、toast、状态同步等。sendPrompt、abortRequest、generatingMessage 或 handleGeneratingMessage。useChatResume
enableAutoResume、isReady、isChatRecordsLoaded、chatBoxData、appId/chatId、generatingMessage、状态同步能力。注意事项:
eventBus、window.message、auto execute effect 暂时可以留在 ChatBox,等 sendPrompt 稳定后再判断是否移动。AbortController 的归属要统一,避免停止按钮只能停一部分请求。activeAppIdRef、activeChatIdRef 用于避免异步回写错误会话,拆 hook 时必须保留。generatingMessage 被发送和恢复共用,应优先稳定其类型和副作用边界。阶段三完成标准:
目标:把记录操作和反馈操作从主组件中移出。
建议提取:
useChatRecordActions
onDelMessageretryInputdelOneMessageuseChatFeedbackActions
onMarkonAddUserLikeonAddUserDislikeonCloseCustomFeedbackonToggleFeedbackReadStatus阶段四完成标准:
onTriggerRefresh。目标:在逻辑边界稳定后,拆 JSX。
建议顺序:
ChatRecordsList
AppChatMain
HomeChatMain
ChatInputArea
ChatInput 包装。ChatBoxModals
阶段五完成标准:
index.tsx 成为编排层,主要组合 hooks 和组件。目标:减少临时兼容代码,完善测试和注释。
工作项:
/** ... */ 函数注释。generatingMessage 默认更新 chatRecords 最后一条 AI 消息。拆分时必须保持:
responseValueId 命中已有 value 时追加,否则新增 value。flowNodeResponse 写入 responseData,不写入 value。workflowDuration 累加并保留两位小数。恢复逻辑必须防止异步结果写入错误会话:
activeAppIdRef 和 activeChatIdRef 的语义必须保留。resumedChatTargetRef 必须继续防止重复恢复。chatBoxData.appId/chatId 必须和 runtime appId/chatId 对齐才恢复。输入状态涉及用户体验,边界包括:
responseText 时恢复原输入。chatInput_${chatId} 草稿不能串到其他会话。当前 ChatBox 监听:
window.postMessage({ type: 'sendPrompt' })EventNameEnum.sendQuestionEventNameEnum.editQuestion拆 hook 时要避免 stale closure:
sendPrompt 必须始终拿到最新 lastInteractive 和 canSendPrompt。外部依赖 ChatBoxRef:
restartChat
chatStarted。scrollToBottom
拆分后 useImperativeHandle 可以留在 index.tsx,避免外部 ref 逻辑分散。
ChatBox 被多个入口复用,验证不能只覆盖一个页面:
AppChatWindowHomeChatWindowchat/share这些入口的 chatType、feedbackType、showMarkIcon、showWorkorder、enableAutoResume 不完全一致。
优先补充以下测试:
recordGroups.test.ts
isExpanded。requestVariables.test.ts
generateMessage.test.ts
resume.test.ts
保留并持续运行现有测试:
pnpm test projects/app/test/components/core/chat/ChatContainer/ChatBox/utils.test.ts
pnpm test projects/app/test/components/core/chat/ChatContainer/ChatBox/scrollUtils.test.ts
每个阶段至少验证:
涉及 useChatResume 时必须额外验证:
全部阶段完成后再考虑:
pnpm lint
pnpm test
cd projects/app && pnpm build
如果中途只是局部拆分,不要求每一步都跑全量测试,但每一步必须至少跑相关局部测试。
recordGroups.ts,提取 log 模式 deleted records 分组逻辑。requestVariables.ts,提取 request variables 格式化逻辑。resume.ts,提取恢复生成相关纯判断。generatingMessage 是否先拆 reducer,再接回 setChatRecords。结论:generatingMessage 可以进一步拆成 event reducer,但它同时牵涉 TTS 分段、滚动跟随、resetVariables 和 setChatRecords 的最后一条 AI 消息约束。为了保持 PR 1 低风险,先不在纯逻辑 PR 中拆它,改放到“阶段三:生成与恢复 hook 提取”里和 useChatGenerate 一起处理。
useChatInputForm。chatStarted、resetInputVal 从 index.tsx 移入 useChatInputForm。useChatScroll。scrollToBottom、generatingScroll 移入 useChatScroll。index.tsx 的生命周期 effect 中,但改为调用 useChatScroll 暴露的 scrollToBottom。useVariableInputVisibility。useQuestionGuide。utils.test.ts、scrollUtils.test.ts 和 PR 1 新增测试。useChatGenerate,输出 sendPrompt、abortRequest、generatingMessage。eventBus、window message、auto execute 行为在 index.tsx,不在 PR 3 同时迁移。useChatResume。utils.test.ts、scrollUtils.test.ts 和 PR 1 新增测试。tsc --noEmit 验证生成/恢复 hook 类型。useChatRecordActions。onDelMessage、retryInput、delOneMessage。useChatFeedbackActions。PR 5 结论:本次只完成删除与重试相关 record action 提取,并通过现有 ChatBox 局部测试与 tsc --noEmit 验证。反馈、标注和 log read status 仍留在 index.tsx,等待下一 PR 单独处理。
PR 6 结论:本次完成反馈与标注 action 提取,并通过现有 ChatBox 局部测试与 tsc --noEmit 验证。modal JSX 仍留在 index.tsx,等待 UI 组件提取阶段处理。
ChatRecordsList。AppChatMain。HomeChatMain:当前 home 分支剩余 JSX 较小,继续拆会增加 props 透传和 review 成本。ChatInputArea:底部输入区直接连接 sendPrompt、abortRequest、lastInteractive、TextareaDom 和 workorder,后续有输入区专项需求时再拆。ChatBoxModals。PR 7 结论:本次只提取 ChatBoxModals,将用户反馈弹窗和管理员标注弹窗从 index.tsx 移出。records list、app/home main 和 input area 仍留给后续 PR。
PR 8 结论:本次只提取 ChatRecordsList,将 records map、deleted collapse、时间分隔、human/AI ChatItem 渲染、custom feedback 和 admin mark 展示从 index.tsx 移出。scroll 容器、app/home main 和 input area 仍留给后续 PR。
PR 9 结论:本次只提取 AppChatMain,将非 home 模式下的 ScrollData、WelcomeBox、VariableInputForm 和 ChatRecordsList 组合从 index.tsx 移出。底部输入区、workorder 和 home 主区域仍留给后续 PR。
PR 10 收敛结论:当前阶段暂不继续拆 HomeChatMain 和 ChatInputArea。ChatBox/index.tsx 已从原来的大组件收敛到编排层,剩余逻辑主要是 context 取数、运行时 hook 编排、home/app 分支、输入区和生命周期 effect。后续如果没有输入区或 home 页专项需求,优先做集成验证,而不是继续机械拆分。
PR 10 验证结论:已运行 pnpm --dir projects/app exec tsc --noEmit --pretty false、ChatBox 现有局部测试和 pnpm --dir projects/app build。build 退出码为 0,过程中仍有现有的 styled-jsx/style.js 解析 warning 和 i18next 初始化提示,不阻塞构建。