20260429_231526-fix: upload field mismatch, WebSocket StrictMode crash, project list refresh after upload
This commit is contained in:
BIN
logo_square.png
BIN
logo_square.png
Binary file not shown.
|
Before Width: | Height: | Size: 99 KiB |
@@ -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();
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
41
工程分析/实现方案-2026-04-29-23-10-27.md
Normal file
41
工程分析/实现方案-2026-04-29-23-10-27.md
Normal 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 分钟
|
||||||
26
工程分析/实现方案-20260429_231526.md
Normal file
26
工程分析/实现方案-20260429_231526.md
Normal 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` ref,cleanup 时置 `false`
|
||||||
|
- `onProgress` callback 与 `setInterval` callback 均检查 `mounted`
|
||||||
|
- 增加 500ms 延迟连接,避免 mount/unmount 过快时创建无意义连接
|
||||||
36
工程分析/测试方案-2026-04-29-23-10-27.md
Normal file
36
工程分析/测试方案-2026-04-29-23-10-27.md
Normal 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
|
||||||
28
工程分析/测试方案-20260429_231526.md
Normal file
28
工程分析/测试方案-20260429_231526.md
Normal 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 连接,无残留报错
|
||||||
26
工程分析/经验记录.md
26
工程分析/经验记录.md
@@ -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` ref,cleanup 时置 `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. 具体问题
|
||||||
|
|||||||
39
工程分析/需求分析-2026-04-29-23-10-27.md
Normal file
39
工程分析/需求分析-2026-04-29-23-10-27.md
Normal 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 交互流程
|
||||||
|
- 最小修改原则
|
||||||
|
|
||||||
|
## 风险评估
|
||||||
|
| 风险点 | 影响 | 缓解措施 |
|
||||||
|
|--------|------|----------|
|
||||||
|
| 上传+创建项目合并后逻辑复杂 | 低 | 先上传,成功后创建项目,两步顺序执行 |
|
||||||
21
工程分析/需求分析-20260429_231526.md
Normal file
21
工程分析/需求分析-20260429_231526.md
Normal 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`
|
||||||
|
- 上传完成后项目列表自动刷新并显示新项目
|
||||||
Reference in New Issue
Block a user