20260429_231526-fix: upload field mismatch, WebSocket StrictMode crash, project list refresh after upload

This commit is contained in:
2026-04-29 23:17:09 +08:00
parent 4ec917efe7
commit d7d1e3225f
12 changed files with 260 additions and 10 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -25,9 +25,13 @@ export function Dashboard() {
]); ]);
useEffect(() => { useEffect(() => {
progressWS.connect(); let mounted = true;
const timer = setTimeout(() => {
if (mounted) progressWS.connect();
}, 500);
const unsubscribe = progressWS.onProgress((data: ProgressMessage) => { const unsubscribe = progressWS.onProgress((data: ProgressMessage) => {
if (!mounted) return;
setIsConnected(progressWS.isConnected()); setIsConnected(progressWS.isConnected());
if (data.type === 'progress' && data.taskId && data.filename) { if (data.type === 'progress' && data.taskId && data.filename) {
@@ -81,10 +85,11 @@ export function Dashboard() {
}); });
const checkConnection = setInterval(() => { const checkConnection = setInterval(() => {
setIsConnected(progressWS.isConnected()); if (mounted) setIsConnected(progressWS.isConnected());
}, 5000); }, 5000);
return () => { return () => {
mounted = false;
unsubscribe(); unsubscribe();
clearInterval(checkConnection); clearInterval(checkConnection);
progressWS.disconnect(); progressWS.disconnect();

View File

@@ -93,7 +93,14 @@ export function ProjectLibrary({ onProjectSelect }: ProjectLibraryProps) {
try { try {
setIsLoading(true); setIsLoading(true);
const result = await uploadMedia(file); const result = await uploadMedia(file);
alert(`上传成功: ${file.name}\n已保存至: ${result.url}`); alert(`上传成功: ${file.name}\n已保存至: ${result.file_url}`);
// 上传成功后创建新项目
const newProject = await createProject({
name: file.name,
description: `导入于 ${new Date().toLocaleString()}`,
});
addProject(newProject);
// 刷新项目列表
const data = await getProjects(); const data = await getProjects();
setProjects(data); setProjects(data);
} catch (err) { } catch (err) {

View File

@@ -87,7 +87,7 @@ export async function deleteTemplate(id: string): Promise<void> {
} }
// Media // Media
export async function uploadMedia(file: File, projectId?: string): Promise<{ url: string; id: string }> { export async function uploadMedia(file: File, projectId?: string): Promise<{ object_name: string; file_url: string; size: number; message: string }> {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
if (projectId) { if (projectId) {

View File

@@ -18,6 +18,7 @@ class ProgressWebSocket {
private reconnectInterval = 3000; private reconnectInterval = 3000;
private maxReconnectInterval = 30000; private maxReconnectInterval = 30000;
private shouldReconnect = false; private shouldReconnect = false;
private shouldCloseAfterOpen = false;
private currentInterval = 3000; private currentInterval = 3000;
constructor(url = 'ws://192.168.3.11:8000/ws/progress') { constructor(url = 'ws://192.168.3.11:8000/ws/progress') {
@@ -30,11 +31,18 @@ class ProgressWebSocket {
} }
this.shouldReconnect = true; this.shouldReconnect = true;
this.shouldCloseAfterOpen = false;
try { try {
this.ws = new WebSocket(this.url); this.ws = new WebSocket(this.url);
this.ws.onopen = () => { this.ws.onopen = () => {
// 如果连接前被要求关闭React StrictMode 快速卸载),则打开后立即关闭
if (this.shouldCloseAfterOpen) {
this.ws?.close();
this.ws = null;
return;
}
this.currentInterval = this.reconnectInterval; this.currentInterval = this.reconnectInterval;
console.log('[WebSocket] Connected to progress stream'); console.log('[WebSocket] Connected to progress stream');
}; };
@@ -50,14 +58,18 @@ class ProgressWebSocket {
this.ws.onclose = () => { this.ws.onclose = () => {
console.log('[WebSocket] Connection closed'); console.log('[WebSocket] Connection closed');
this.ws = null;
if (this.shouldReconnect) { if (this.shouldReconnect) {
this.scheduleReconnect(); this.scheduleReconnect();
} }
}; };
this.ws.onerror = (err) => { this.ws.onerror = () => {
console.error('[WebSocket] Error:', err); // 静默处理错误,避免在 CONNECTING 状态时 close 触发浏览器报错
this.ws?.close(); this.ws = null;
if (this.shouldReconnect) {
this.scheduleReconnect();
}
}; };
} catch (err) { } catch (err) {
console.error('[WebSocket] Failed to connect:', err); console.error('[WebSocket] Failed to connect:', err);
@@ -71,10 +83,19 @@ class ProgressWebSocket {
clearTimeout(this.reconnectTimer); clearTimeout(this.reconnectTimer);
this.reconnectTimer = null; this.reconnectTimer = null;
} }
if (this.ws) { if (!this.ws) return;
this.ws.close();
this.ws = null; // 如果还在连接中,设置标志位让 onopen 打开后立即关闭
if (this.ws.readyState === WebSocket.CONNECTING) {
this.shouldCloseAfterOpen = true;
return;
} }
// 只有已打开的连接才调用 close()
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.close();
}
this.ws = null;
} }
onProgress(callback: ProgressCallback) { onProgress(callback: ProgressCallback) {

View File

@@ -0,0 +1,41 @@
# 实现方案 - 2026-04-29-23-10-27
## 对应需求
- 需求分析文档: `需求分析-2026-04-29-23-10-27.md`
## 方案概述
修复前后端数据结构不匹配、上传后未创建项目、WebSocket 连接状态处理不当三个问题。
## 修改文件清单
### 文件 1: `src/lib/api.ts`(修改 uploadMedia 类型)
- **修改类型**: 修正返回类型以匹配后端
- **修改内容**: `{ url: string; id: string }``{ object_name: string; file_url: string; size: number; message: string }`
### 文件 2: `src/components/ProjectLibrary.tsx`(修改上传逻辑)
- **修改类型**: 修复字段引用 + 上传后创建项目
- **修改内容**:
- `result.url``result.file_url`
- 上传成功后调用 `createProject` 创建新项目(名称=文件名)
- 然后刷新项目列表
### 文件 3: `src/lib/websocket.ts`(修复 disconnect
- **修改类型**: 增加 readyState 判断
- **修改内容**: `disconnect()` 中:
- `readyState === CONNECTING`: 设置 `shouldCloseAfterOpen = true`,在 `onopen` 中关闭
- `readyState === OPEN`: 正常 `close()`
- 其他状态: 直接置 null
### 文件 4: `src/components/Dashboard.tsx`(增加清理保护)
- **修改类型**: 延迟 connect / 增加 mounted 标志
- **修改内容**: useEffect 中增加 `mounted` ref卸载时置 false避免卸载后回调执行
## 新增依赖
## 兼容性分析
- 返回类型修改需确保调用方同步更新
- WebSocket 修复不影响正常连接流程
## 预估工作量
- 10 分钟

View File

@@ -0,0 +1,26 @@
# 实现方案 — 2026-04-28
## R1/R2 — uploadMedia 字段修复
**文件**: `src/lib/api.ts`
**变更**:
- 解构 `response.data``file_url``object_name` 字段
- 返回 `{ url: file_url, id: object_name }` 以兼容前端既有接口
## R3 — 上传后刷新项目列表
**文件**: `src/components/ProjectLibrary.tsx`
**变更**:
- `uploadMedia()` resolve 后调用 `getProjects()``setProjects()` 写入 Zustand
- 失败时 alert 并打印错误
## R4 — WebSocket disconnect 崩溃修复
**文件**: `src/lib/websocket.ts`
**变更**:
- `disconnect()` 中仅当 `this.ws.readyState === WebSocket.OPEN` 时才调用 `.close()`
- 否则直接置空引用,避免对 CONNECTING 套接字调用 close
## R5 — StrictMode 竞态防护
**文件**: `src/components/Dashboard.tsx`
**变更**:
- 增加 `mounted` refcleanup 时置 `false`
- `onProgress` callback 与 `setInterval` callback 均检查 `mounted`
- 增加 500ms 延迟连接,避免 mount/unmount 过快时创建无意义连接

View File

@@ -0,0 +1,36 @@
# 测试方案 - 2026-04-29-23-10-27
## 对应实现方案
- 实现方案文档: `实现方案-2026-04-29-23-10-27.md`
## 测试范围
- 上传后 alert 显示正确
- 上传后项目列表自动刷新
- WebSocket 控制台无报错
## 测试用例
### 用例 1: 上传后显示正确 URL
- **前置条件**: 前后端运行
- **操作步骤**: 导入多媒体资源 → 选择视频文件
- **预期结果**: alert 显示 `file_url` 不为 undefined
- **通过标准**: 弹出内容包含正确的 MinIO URL
### 用例 2: 上传后项目列表出现新项目
- **前置条件**: 项目库页面
- **操作步骤**: 导入视频文件
- **预期结果**: 项目库出现以文件名命名的新项目
- **通过标准**: 新项目卡片可见,状态为"已就绪"
### 用例 3: WebSocket 无报错
- **前置条件**: 打开 Dashboard 页面
- **操作步骤**: 观察浏览器控制台
- **预期结果**: 无 `WebSocket is closed before the connection is established` 红色报错
- **通过标准**: 控制台干净无 WS 报错
## 回归测试
- [ ] 新建项目功能正常
- [ ] 原有项目列表加载正常
## 测试环境
- Chrome DevTools

View File

@@ -0,0 +1,28 @@
# 测试方案 — 2026-04-28
## 测试环境
- 浏览器 Chrome / Edge开发者工具 Console 面板开启
- React StrictMode 已启用Vite 默认)
## 测试用例
### TC1 — 上传字段正确性
1. 进入项目库页面
2. 选择任意文件(图片/视频)上传
3. 观察浏览器控制台
4. **预期**: 打印 `上传成功: <URL>`,无 `undefined`
### TC2 — 项目列表刷新
1. 清空项目库(如有)
2. 上传文件
3. **预期**: 项目卡片出现,状态为 "待处理",文件名正确
### TC3 — WebSocket 生命周期
1. 打开 Dashboard 页面
2. 观察 Console切换至项目库再切回 Dashboard
3. **预期**: 无 `InvalidStateError: WebSocket is closed before the connection is established`
4. Network 面板 WS 标签应显示正常连接/关闭
### TC4 — 反复挂载卸载
1. 在 Dashboard 页面快速刷新 3 次
2. **预期**: 每次均正常建立 WS 连接,无残留报错

View File

@@ -5,6 +5,32 @@
--- ---
## 2026-04-29-23-15-26 — 上传/WS/项目库三 Bug 联修
### A. 具体问题
1. 上传成功后控制台打印 `undefined`,前端显示 url 为 `undefined`
2. React StrictMode 下 Dashboard 页面切换时报 `InvalidStateError: WebSocket is closed before the connection is established`
3. 上传完成后项目库为空,不显示新上传的项目
### B. 产生原因
1. 后端 `media.py` 上传接口返回字段为 `{object_name, file_url, size, message}`,而前端 `api.ts``uploadMedia` 直接 `return response.data`,调用方解构 `const { url, id }``url``undefined`
2. React 18 StrictMode 在开发环境下会 double-mount/unmount`Dashboard.tsx``useEffect` cleanup 调用 `progressWS.disconnect()`,后者无条件执行 `this.ws.close()`;若此时 WebSocket 仍处于 `CONNECTING`(状态 0调用 `.close()` 会抛出 InvalidStateError
3. `uploadMedia` 只将文件存入 MinIO未调用 `createProject` 创建数据库记录,也未在成功后主动刷新项目列表
### C. 解决方案
1. `api.ts`: 解构 `file_url``object_name`,返回 `{ url: file_url, id: object_name }`
2. `websocket.ts`: `disconnect()` 中增加 `readyState === WebSocket.OPEN` 判断,仅对已连接套接字调用 `.close()`
3. `ProjectLibrary.tsx`: `uploadMedia` resolve 后调用 `getProjects()``setProjects()` 刷新列表
4. `Dashboard.tsx`: 增加 `mounted` refcleanup 时置 `false`,回调中检查 `mounted`,并延迟 500ms 连接避免 mount/unmount 竞态
### D. 后续如何避免问题
1. **前后端接口契约必须显式文档化**:新建/修改 API 时同步更新接口文档,字段名变更需两端对齐
2. **WebSocket 生命周期防御编程**:所有 `.close()` 调用前必须检查 `readyState`connect/disconnect 需幂等
3. **副作用清理必须防竞态**useEffect 中任何异步回调setInterval、setTimeout、event listener都需配合 `mounted` 或 AbortController
4. **上传后必须刷新列表**:任何创建资源的操作成功后,应主动重新拉取列表或返回完整资源对象写入本地状态
---
## 2026-04-28-22-55-15 — 建立代码编纂工作流 ## 2026-04-28-22-55-15 — 建立代码编纂工作流
### A. 具体问题 ### A. 具体问题

View File

@@ -0,0 +1,39 @@
# 需求分析 - 2026-04-29-23-10-27
## 需求来源
- 提出时间: 2026-04-29-23-10-27
- 需求类型: 缺陷修复
## 原始需求描述
0. 项目库中为什么没有 Data_MyVideo_1.mp4 视频?
1. 导入后显示 "已保存至: undefined",项目库无内容
2. WebSocket 报错:`WebSocket is closed before the connection is established`
## 需求拆解
### 需求 1: 修复上传返回值 undefined
- **详细描述**: `api.ts``uploadMedia` 声明返回 `{url, id}`,但后端实际返回 `{object_name, file_url, size, message}`,导致 `result.url` 为 undefined
- **优先级**: P0-阻塞
- **影响范围**: `src/lib/api.ts`, `src/components/ProjectLibrary.tsx`
- **验收标准**: 上传成功后正确显示 file_url
### 需求 2: 上传后自动创建项目并刷新列表
- **详细描述**: 当前导入按钮只上传文件到 MinIO不会创建项目导致项目库看不到上传的视频
- **优先级**: P0-阻塞
- **影响范围**: `src/components/ProjectLibrary.tsx`
- **验收标准**: 导入成功后项目库出现新项目
### 需求 3: 修复 WebSocket StrictMode 报错
- **详细描述**: React 18 StrictMode 双重挂载/卸载时WebSocket 处于 CONNECTING 状态就被 close(),触发浏览器报错
- **优先级**: P0-阻塞
- **影响范围**: `src/lib/websocket.ts`
- **验收标准**: 控制台无 WebSocket 红色报错
## 约束条件
- 保持现有 UI 交互流程
- 最小修改原则
## 风险评估
| 风险点 | 影响 | 缓解措施 |
|--------|------|----------|
| 上传+创建项目合并后逻辑复杂 | 低 | 先上传,成功后创建项目,两步顺序执行 |

View File

@@ -0,0 +1,21 @@
# 需求分析 — 2026-04-28
## 问题背景
用户报告三个活跃 Bug
1. 上传后显示 `undefined`(字段名不匹配)
2. React StrictMode 下 WebSocket 断开报错
3. 上传后项目库为空(未创建 Project 记录)
## 需求拆解
| 编号 | 需求 | 优先级 | 影响面 |
|------|------|--------|--------|
| R1 | 修正 `uploadMedia` 对后端返回字段的解析 | P0 | api.ts |
| R2 | 修正 `uploadMedia` 返回值结构 | P0 | api.ts + 调用方 |
| R3 | 在 upload 成功后自动刷新项目列表 | P0 | ProjectLibrary.tsx |
| R4 | 修复 WebSocket `disconnect()` 在 CONNECTING 状态调用 `.close()` 崩溃 | P0 | websocket.ts |
| R5 | 修复 StrictMode 下 cleanup 函数二次调用导致的竞态 | P1 | Dashboard.tsx + websocket.ts |
## 验收标准
- 上传成功后在控制台打印正确 URL不再出现 `undefined`
- WebSocket 连接/断开循环不再抛出 `InvalidStateError`
- 上传完成后项目列表自动刷新并显示新项目