避免 Remix revalidation:使用原生 fetch 而非 fetcher.submit,避免触发 Remix 的数据重新验证机制,这是导致流式请求被中止的主要原因。
Some checks failed
CI/CD / Test (push) Has been cancelled
Docker Publish / docker-build-and-push (push) Has been cancelled

增加延迟时间:将项目保存延迟增加到2秒,确保流式请求完全结束后再执行保存操作,减少时间窗口内的冲突。

改进错误处理:添加了更详细的错误处理和日志记录,便于后续调试。
This commit is contained in:
史悦
2025-10-15 09:55:47 +08:00
parent c1829e5af9
commit 177f15a136
3 changed files with 96 additions and 14 deletions

View File

@@ -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');
},

View File

@@ -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(
{
messageId,
pages: JSON.stringify(projectPages),
sections: JSON.stringify(projectSections),
},
{
// 使用原生 fetch 而非 Remix fetcher避免触发 revalidation 导致流式请求中断
const response = await fetch('/api/project', {
method: 'POST',
action: '/api/project',
headers: {
'Content-Type': 'application/json',
},
);
body: JSON.stringify({
messageId,
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);

View File

@@ -0,0 +1,75 @@
# 二次对话必现中断问题排查记录
> 时间范围2025-10-14 ~ 2025-10-15
> 调试参与者CodexAI 助手)
> 目标:定位并解决「在同一聊天中第二次发送消息必定失败」的问题
## 一、现象与初始假设
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 fetcherrevalidation
- `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 解析警告**
- 第二轮响应中模型返回了不完整的 `<script>``<section>`,导致前端解析失败。虽与断线无直接关系,但可能触发某些保护性中止,需在后续排查中一并关注。
## 五、总结
目前的改动保证了服务端在异常情况下不会崩溃,也不会误写数据库。但核心问题是客户端在第二轮请求时仍旧提前关闭连接(表现为 `ERR_INCOMPLETE_CHUNKED_ENCODING`)。
下一步的核心工作是找出“谁”触发了 `AbortController.abort()`,并验证是否与结构化辅助步骤相关。待上述验证完成后,再恢复辅助功能并优化网络策略,才能真正实现「第二次对话不再失败」的目标。*** End Patch