From 177f15a1365300de381cd7ba5a03f909cee60adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E6=82=A6?= Date: Wed, 15 Oct 2025 09:55:47 +0800 Subject: [PATCH] =?UTF-8?q?=E9=81=BF=E5=85=8D=20Remix=20revalidation?= =?UTF-8?q?=EF=BC=9A=E4=BD=BF=E7=94=A8=E5=8E=9F=E7=94=9F=20fetch=20?= =?UTF-8?q?=E8=80=8C=E9=9D=9E=20fetcher.submit=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E8=A7=A6=E5=8F=91=20Remix=20=E7=9A=84=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E9=87=8D=E6=96=B0=E9=AA=8C=E8=AF=81=E6=9C=BA=E5=88=B6=EF=BC=8C?= =?UTF-8?q?=E8=BF=99=E6=98=AF=E5=AF=BC=E8=87=B4=E6=B5=81=E5=BC=8F=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E8=A2=AB=E4=B8=AD=E6=AD=A2=E7=9A=84=E4=B8=BB=E8=A6=81?= =?UTF-8?q?=E5=8E=9F=E5=9B=A0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加延迟时间:将项目保存延迟增加到2秒,确保流式请求完全结束后再执行保存操作,减少时间窗口内的冲突。 改进错误处理:添加了更详细的错误处理和日志记录,便于后续调试。 --- app/.client/hooks/useChatMessage.ts | 3 +- app/.client/hooks/useProject.ts | 32 +++++++----- chat-second-request-debug.md | 75 +++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 chat-second-request-debug.md diff --git a/app/.client/hooks/useChatMessage.ts b/app/.client/hooks/useChatMessage.ts index 2726409..c2c2782 100644 --- a/app/.client/hooks/useChatMessage.ts +++ b/app/.client/hooks/useChatMessage.ts @@ -65,10 +65,11 @@ export function useChatMessage({ addStoppedProgressMessage('网络连接中断,响应已停止'); }, onFinish: ({ message }) => { + // 增加延迟,避免与当前流式请求冲突 setTimeout(() => { // 保存 editor project saveProject(message.id); - }, SAVE_PROJECT_DELAY_MS); + }, SAVE_PROJECT_DELAY_MS * 2); // 延迟时间加倍到2秒 refreshUsageStats(); logger.debug('Finished streaming'); }, diff --git a/app/.client/hooks/useProject.ts b/app/.client/hooks/useProject.ts index 6e79a69..07c2cff 100644 --- a/app/.client/hooks/useProject.ts +++ b/app/.client/hooks/useProject.ts @@ -1,4 +1,3 @@ -import { useFetcher } from '@remix-run/react'; import { useEditorStorage } from '~/.client/persistence/editor'; import { webBuilderStore } from '~/.client/stores/web-builder'; import type { ApiResponse } from '~/types/global'; @@ -7,7 +6,6 @@ import { createScopedLogger } from '~/utils/logger'; const logger = createScopedLogger('useGrapesProject'); export function useProject() { - const fetcher = useFetcher(); const { saveEditorProject } = useEditorStorage(); /** @@ -62,18 +60,26 @@ export function useProject() { // 先保存在本地数据中 saveEditorProject(messageId, projectPages, projectSections); // 再调用远程接口保存到后端数据库 - // 使用fetcher调用API保存项目数据 - fetcher.submit( - { + // 使用原生 fetch 而非 Remix fetcher,避免触发 revalidation 导致流式请求中断 + const response = await fetch('/api/project', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ messageId, - pages: JSON.stringify(projectPages), - sections: JSON.stringify(projectSections), - }, - { - method: 'POST', - action: '/api/project', - }, - ); + pages: projectPages, + sections: projectSections, + }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + logger.error('保存项目失败:', errorData.message || '服务器错误'); + return false; + } + + logger.info('项目保存成功'); return true; } catch (error) { logger.error('保存GrapesJS项目失败:', error); diff --git a/chat-second-request-debug.md b/chat-second-request-debug.md new file mode 100644 index 0000000..3abbe8a --- /dev/null +++ b/chat-second-request-debug.md @@ -0,0 +1,75 @@ +# 二次对话必现中断问题排查记录 + +> 时间范围:2025-10-14 ~ 2025-10-15 +> 调试参与者:Codex(AI 助手) +> 目标:定位并解决「在同一聊天中第二次发送消息必定失败」的问题 + +## 一、现象与初始假设 + +1. **复现特征** + - 第一次对话正常生成页面与 Section,并成功保存。 + - 紧接着第二次对话必定失败,前端提示 `net::ERR_INCOMPLETE_CHUNKED_ENCODING` 或返回 502。 + - 服务端日志显示 `request.signal` 被中止,随后触发 `/api/project` 保存失败并抛出 `P2003`。 + +2. **初步假设** + - 可能是浏览器主动 abort 导致 SSE 连接中断。 + - 也可能是结构化摘要(structuredPageSnapshot)等辅助调用耗时太长,引发代理或服务器超时。 + - `/api/project` 写库失败属于连锁反应,需要隔离观测。 + +## 二、针对性尝试与结果 + +### 1. 前端日志打点 +- 为 `fetch`、`AbortController.abort`、`useChat` 状态变更以及 `stream-data` 增加详尽日志。 +- 结果:确认 `ERR_INCOMPLETE_CHUNKED_ENCODING` 发生时,浏览器确实收到 200 响应头后被动断流;`AbortController.abort` 堆栈来自 Remix fetcher 的 `submit`(revalidation),并非我们主动调用。 + +### 2. 阻断 `/api/project` 的连锁影响 +- 在 `useChatMessage.onFinish` 与 `useProject.saveProject` 中加入 `skipNextProjectSaveRef` 与 `getAborted()` 检查,避免失败时继续提交 `/api/project`,消除 `P2003` 噪声。 +- 验证:失败后不再触发保存请求,日志干净。 + +### 3. 结构化摘要调用的中止处理 +- 在 `structuredPageSnapshot` 周围增加 `AbortController`,尝试在前端 abort 时终止重试。 +- 初始实现将自定义 `AbortError` 抛到外层,导致 Node 进程崩溃。修复后改用 `retry.AbortError` 并吞掉异常,确保只记录 warning,不崩溃。 + +### 4. 服务端额外日志与防护 +- 捕获 `chatStreamText`、`structuredPageSnapshot` 的耗时及异常堆栈,判断是否是上游 LLM 调用超时。 +- 在 `streamExecutor` 与 `chatStreamText` 内部检测 `request.signal.aborted` 后立即返回,防止中止后继续执行后续逻辑。 +- 处理 `trust proxy` 设置,避免限流中间件报错阻塞服务启动。 + +### 5. 观察结果 +- 中止场景下服务器不再崩溃,也不会触发 `/api/project`,但「第二次请求仍然被客户端中止」这一现象未根治。 +- 前端控制台依旧会在第二轮输出 `ERR_INCOMPLETE_CHUNKED_ENCODING`,说明连接被关闭但没有崩溃日志。 + +## 三、现阶段确认的结论 + +1. **崩溃原因已排除** + - 之前 Node 进程重启是因为我们抛出的 `Error("客户端已中止…")` 浮出到最外层,现在已改为 `retry.AbortError` 并在 catch 内消化,服务端不会再因此崩溃。 + +2. **保存数据的连锁反应已阻断** + - 二次对话失败不会再触发 `/api/project` 的 500 与 `P2003`,数据库层面不受影响。 + +3. **核心断流仍存在** + - 浏览器在第二次流式请求开始不久就断开连接,`AbortController.abort` 的堆栈指向 Remix fetcher(revalidation)。 + - `structuredPageSnapshot`(辅助模型调用)在每次中止时都会被打断,并记录 warning;但即便完全不保存也会早早中止,说明问题不在写库阶段,而是在请求处理链更早的位置。 + +## 四、待验证与下一步计划 + +1. **验证结构化阶段是否必要条件** + - 临时跳过 `structuredPageSnapshot` 和 `selectContext`,仅保留主模型回复,看二次对话是否仍断流。 + - 若断流消失,则将进一步评估结构化内容的超时策略或输入长度限制。 + +2. **获取 Abort 堆栈** + - 在浏览器控制台记录 `[AbortController.abort]` 的堆栈(我们已注入补丁),用于确认到底是 Remix fetcher 还是其他逻辑主动取消请求。 + - 若堆栈指向 Remix revalidation,可考虑调整 Remix 的 `shouldRevalidate` 或 fetcher 调用时机。 + +3. **查看上游网络/代理** + - 目前日志没有出现 502,但仍报 `ERR_INCOMPLETE_CHUNKED_ENCODING`,需要确认 Docker/宿主机是否存在连接重置(本地端口、代理等)。 + - 可以通过 `curl -N` 直接请求 `/api/chat` 进行 CLI 流式验证,排除浏览器因素。 + +4. **针对 HTML 解析警告** + - 第二轮响应中模型返回了不完整的 `