2026-04-29-22-49-38 - 修复WebSocket连接+项目状态异常+导入按钮无响应+后端WS路由
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from config import settings
|
from config import settings
|
||||||
@@ -81,3 +81,59 @@ app.include_router(export.router)
|
|||||||
def health_check() -> dict:
|
def health_check() -> dict:
|
||||||
"""Health check endpoint."""
|
"""Health check endpoint."""
|
||||||
return {"status": "ok", "service": "SegServer"}
|
return {"status": "ok", "service": "SegServer"}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# WebSocket: 实时进度推送
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class ConnectionManager:
|
||||||
|
"""Manage WebSocket connections for progress broadcasting."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.active_connections: list[WebSocket] = []
|
||||||
|
|
||||||
|
async def connect(self, websocket: WebSocket):
|
||||||
|
await websocket.accept()
|
||||||
|
self.active_connections.append(websocket)
|
||||||
|
logger.info("WebSocket client connected. Total: %d", len(self.active_connections))
|
||||||
|
|
||||||
|
def disconnect(self, websocket: WebSocket):
|
||||||
|
if websocket in self.active_connections:
|
||||||
|
self.active_connections.remove(websocket)
|
||||||
|
logger.info("WebSocket client disconnected. Total: %d", len(self.active_connections))
|
||||||
|
|
||||||
|
async def broadcast(self, message: dict):
|
||||||
|
"""Broadcast a message to all connected clients."""
|
||||||
|
for connection in self.active_connections.copy():
|
||||||
|
try:
|
||||||
|
await connection.send_json(message)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("WebSocket send failed: %s", exc)
|
||||||
|
self.disconnect(connection)
|
||||||
|
|
||||||
|
|
||||||
|
manager = ConnectionManager()
|
||||||
|
|
||||||
|
|
||||||
|
@app.websocket("/ws/progress")
|
||||||
|
async def websocket_progress(websocket: WebSocket):
|
||||||
|
"""WebSocket endpoint for real-time parsing/AI progress updates."""
|
||||||
|
await manager.connect(websocket)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
# Receive client messages (heartbeat / subscription requests)
|
||||||
|
data = await websocket.receive_text()
|
||||||
|
logger.debug("WebSocket received: %s", data)
|
||||||
|
|
||||||
|
# Echo heartbeat to keep connection alive
|
||||||
|
await websocket.send_json({
|
||||||
|
"type": "status",
|
||||||
|
"status": "connected",
|
||||||
|
"message": "Progress stream active",
|
||||||
|
"timestamp": str(logging.time.time() if hasattr(logging, 'time') else __import__('time').time()),
|
||||||
|
})
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
manager.disconnect(websocket)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("WebSocket error: %s", exc)
|
||||||
|
manager.disconnect(websocket)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class Project(Base):
|
|||||||
name = Column(String(255), nullable=False)
|
name = Column(String(255), nullable=False)
|
||||||
description = Column(Text, nullable=True)
|
description = Column(Text, nullable=True)
|
||||||
video_path = Column(String(512), nullable=True)
|
video_path = Column(String(512), nullable=True)
|
||||||
status = Column(String(50), default="pending", nullable=False)
|
status = Column(String(50), default="Ready", nullable=False)
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(
|
updated_at = Column(
|
||||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { UploadCloud, Film, Settings2, MoreHorizontal, Plus, Loader2 } from 'lucide-react';
|
import { UploadCloud, Film, Settings2, MoreHorizontal, Plus, Loader2 } from 'lucide-react';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
import { useStore } from '../store/useStore';
|
import { useStore } from '../store/useStore';
|
||||||
import { getProjects, createProject } from '../lib/api';
|
import { getProjects, createProject, uploadMedia } from '../lib/api';
|
||||||
import type { Project } from '../store/useStore';
|
import type { Project } from '../store/useStore';
|
||||||
|
|
||||||
interface ProjectLibraryProps {
|
interface ProjectLibraryProps {
|
||||||
@@ -19,6 +19,7 @@ export function ProjectLibrary({ onProjectSelect }: ProjectLibraryProps) {
|
|||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [newName, setNewName] = useState('');
|
const [newName, setNewName] = useState('');
|
||||||
const [newDesc, setNewDesc] = useState('');
|
const [newDesc, setNewDesc] = useState('');
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -74,10 +75,36 @@ export function ProjectLibrary({ onProjectSelect }: ProjectLibraryProps) {
|
|||||||
<Plus size={18} />
|
<Plus size={18} />
|
||||||
<span>新建项目</span>
|
<span>新建项目</span>
|
||||||
</button>
|
</button>
|
||||||
<button className="flex items-center gap-2 bg-cyan-600 hover:bg-cyan-500 text-white px-5 py-2.5 rounded-lg font-medium text-sm transition-colors border border-cyan-500 shadow-lg shadow-cyan-900/20">
|
<button
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
className="flex items-center gap-2 bg-cyan-600 hover:bg-cyan-500 text-white px-5 py-2.5 rounded-lg font-medium text-sm transition-colors border border-cyan-500 shadow-lg shadow-cyan-900/20"
|
||||||
|
>
|
||||||
<UploadCloud size={18} />
|
<UploadCloud size={18} />
|
||||||
<span>导入多媒体资源</span>
|
<span>导入多媒体资源</span>
|
||||||
</button>
|
</button>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
className="hidden"
|
||||||
|
accept="video/*,image/*,.dcm"
|
||||||
|
onChange={async (e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const result = await uploadMedia(file);
|
||||||
|
alert(`上传成功: ${file.name}\n已保存至: ${result.url}`);
|
||||||
|
const data = await getProjects();
|
||||||
|
setProjects(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Upload failed:', err);
|
||||||
|
alert('上传失败,请检查后端服务');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -102,10 +129,12 @@ export function ProjectLibrary({ onProjectSelect }: ProjectLibraryProps) {
|
|||||||
{proj.fps || '30FPS'}
|
{proj.fps || '30FPS'}
|
||||||
</span>
|
</span>
|
||||||
<span className="backdrop-blur-md bg-black/40 text-gray-200 text-[10px] px-2 py-1 rounded border border-white/10 flex items-center gap-1 uppercase tracking-widest">
|
<span className="backdrop-blur-md bg-black/40 text-gray-200 text-[10px] px-2 py-1 rounded border border-white/10 flex items-center gap-1 uppercase tracking-widest">
|
||||||
{proj.status === 'Ready' ? (
|
{proj.status === 'Ready' || proj.status === 'ready' ? (
|
||||||
<><div className="w-1.5 h-1.5 bg-emerald-500 rounded-full" /> 已就绪</>
|
<><div className="w-1.5 h-1.5 bg-emerald-500 rounded-full" /> 已就绪</>
|
||||||
) : proj.status === 'Parsing' ? (
|
) : proj.status === 'Parsing' || proj.status === 'parsing' ? (
|
||||||
<><div className="w-1.5 h-1.5 bg-amber-500 rounded-full animate-pulse" /> 解析拆帧中</>
|
<><div className="w-1.5 h-1.5 bg-amber-500 rounded-full animate-pulse" /> 解析拆帧中</>
|
||||||
|
) : proj.status === 'pending' || proj.status === 'Pending' ? (
|
||||||
|
<><div className="w-1.5 h-1.5 bg-blue-500 rounded-full" /> 待处理</>
|
||||||
) : (
|
) : (
|
||||||
<><div className="w-1.5 h-1.5 bg-red-500 rounded-full" /> 异常</>
|
<><div className="w-1.5 h-1.5 bg-red-500 rounded-full" /> 异常</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class ProgressWebSocket {
|
|||||||
private shouldReconnect = false;
|
private shouldReconnect = false;
|
||||||
private currentInterval = 3000;
|
private currentInterval = 3000;
|
||||||
|
|
||||||
constructor(url = 'ws://localhost:8000/ws/progress') {
|
constructor(url = 'ws://192.168.3.11:8000/ws/progress') {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
51
工程分析/实现方案-2026-04-29-22-49-38.md
Normal file
51
工程分析/实现方案-2026-04-29-22-49-38.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# 实现方案 - 2026-04-29-22-49-38
|
||||||
|
|
||||||
|
## 对应需求
|
||||||
|
- 需求分析文档: `需求分析-2026-04-29-22-49-38.md`
|
||||||
|
|
||||||
|
## 方案概述
|
||||||
|
修复 4 个关联缺陷:WebSocket 连接地址错误、后端缺少 WebSocket 路由、导入按钮无交互、项目状态映射不匹配。
|
||||||
|
|
||||||
|
## 修改文件清单
|
||||||
|
|
||||||
|
### 文件 1: `src/lib/websocket.ts`(修改)
|
||||||
|
- **修改类型**: 修改 URL
|
||||||
|
- **修改内容**: `ws://localhost:8000/ws/progress` → `ws://192.168.3.11:8000/ws/progress`
|
||||||
|
|
||||||
|
### 文件 2: `backend/main.py`(修改)
|
||||||
|
- **修改类型**: 新增 WebSocket 路由
|
||||||
|
- **修改内容**: 添加 `@app.websocket("/ws/progress")` 路由,支持:
|
||||||
|
- 客户端连接/断开日志
|
||||||
|
- 接收前端订阅消息
|
||||||
|
- 定期推送心跳(可选)
|
||||||
|
- 通过 Redis 发布/订阅桥接解析进度(预留接口)
|
||||||
|
|
||||||
|
### 文件 3: `src/components/ProjectLibrary.tsx`(修改)
|
||||||
|
- **修改类型**: 添加文件导入交互
|
||||||
|
- **修改内容**:
|
||||||
|
- 添加 `fileInputRef` 引用
|
||||||
|
- 为"导入多媒体资源"按钮添加 `onClick` 触发文件选择器
|
||||||
|
- 添加隐藏的 `<input type="file">`,支持 `accept="video/*,image/*"`
|
||||||
|
- `onChange` 时调用 `uploadMedia` API 上传文件
|
||||||
|
- 上传成功后刷新项目列表
|
||||||
|
|
||||||
|
### 文件 4: `backend/models.py`(修改)
|
||||||
|
- **修改类型**: 修改默认值
|
||||||
|
- **修改内容**: `Project.status` 默认值从 `"pending"` 改为 `"Ready"`
|
||||||
|
- **替代方案**: 若数据库已创建,同时修改 `schemas.py` 中返回状态映射
|
||||||
|
|
||||||
|
### 文件 5: `src/components/ProjectLibrary.tsx`(修改状态显示)
|
||||||
|
- **修改类型**: 扩展状态分支
|
||||||
|
- **修改内容**: 增加对 `'pending'` 状态的显示("待处理")
|
||||||
|
|
||||||
|
## 新增依赖
|
||||||
|
无
|
||||||
|
|
||||||
|
## 兼容性分析
|
||||||
|
- WebSocket 为新增路由,不影响现有 HTTP API
|
||||||
|
- 文件上传复用已有 `uploadMedia` API
|
||||||
|
- 状态默认值修改仅影响新项目
|
||||||
|
|
||||||
|
## 预估工作量
|
||||||
|
- 代码修改: 10 分钟
|
||||||
|
- 重启验证: 5 分钟
|
||||||
47
工程分析/测试方案-2026-04-29-22-49-38.md
Normal file
47
工程分析/测试方案-2026-04-29-22-49-38.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# 测试方案 - 2026-04-29-22-49-38
|
||||||
|
|
||||||
|
## 对应实现方案
|
||||||
|
- 实现方案文档: `实现方案-2026-04-29-22-49-38.md`
|
||||||
|
|
||||||
|
## 测试范围
|
||||||
|
- WebSocket 连接可达性
|
||||||
|
- 文件导入按钮交互
|
||||||
|
- 项目状态显示正确性
|
||||||
|
|
||||||
|
## 测试用例
|
||||||
|
|
||||||
|
### 用例 1: WebSocket 连接测试
|
||||||
|
- **前置条件**: 前后端均运行
|
||||||
|
- **操作步骤**:
|
||||||
|
```bash
|
||||||
|
curl -s -N -H "Connection: Upgrade" -H "Upgrade: websocket" \
|
||||||
|
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
|
||||||
|
-H "Sec-WebSocket-Version: 13" \
|
||||||
|
http://192.168.3.11:8000/ws/progress
|
||||||
|
```
|
||||||
|
- **预期结果**: 返回 101 Switching Protocols
|
||||||
|
- **通过标准**: HTTP 101,连接建立
|
||||||
|
|
||||||
|
### 用例 2: 文件导入按钮交互验证(浏览器)
|
||||||
|
- **前置条件**: 前端运行
|
||||||
|
- **操作步骤**:
|
||||||
|
1. 打开项目库页面
|
||||||
|
2. 点击"导入多媒体资源"按钮
|
||||||
|
- **预期结果**: 弹出系统文件选择器
|
||||||
|
- **通过标准**: 文件选择器出现,支持选择视频/图片
|
||||||
|
|
||||||
|
### 用例 3: 新建项目状态显示
|
||||||
|
- **前置条件**: 后端运行
|
||||||
|
- **操作步骤**:
|
||||||
|
1. 创建新项目
|
||||||
|
2. 查看项目卡片状态标签
|
||||||
|
- **预期结果**: 状态显示为"已就绪"(绿色)
|
||||||
|
- **通过标准**: 非"异常"(红色)
|
||||||
|
|
||||||
|
## 回归测试
|
||||||
|
- [ ] localhost 开发方式不受影响
|
||||||
|
- [ ] 现有项目列表加载正常
|
||||||
|
|
||||||
|
## 测试环境
|
||||||
|
- 浏览器: Chrome
|
||||||
|
- 访问地址: http://192.168.3.11:3000
|
||||||
30
工程分析/经验记录.md
30
工程分析/经验记录.md
@@ -142,4 +142,34 @@ AI 助手运行的容器/环境与项目实际开发环境分离,后者才装
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-04-29-22-49-38 — WebSocket 404 + 项目状态异常 + 导入按钮无响应
|
||||||
|
|
||||||
|
### A. 具体问题
|
||||||
|
1. 控制台报错 `WebSocket connection to 'ws://localhost:8000/ws/progress' failed`
|
||||||
|
2. 项目库中"测试视频项目"状态显示为"异常"(红色)
|
||||||
|
3. "导入多媒体资源"按钮点击无反应
|
||||||
|
|
||||||
|
### B. 产生原因
|
||||||
|
1. `websocket.ts` 仍为 `ws://localhost:8000/ws/progress`,未随前端 baseURL 一起改为服务器 IP
|
||||||
|
2. FastAPI 后端未实现 `/ws/progress` WebSocket 路由
|
||||||
|
3. Uvicorn 缺少 WebSocket 支持库(`websockets` 或 `wsproto` 未安装)
|
||||||
|
4. 后端 `Project` 模型默认状态为 `"pending"`,前端只识别 `"Ready"` / `"Parsing"`,其他状态均显示"异常"
|
||||||
|
5. `ProjectLibrary.tsx` 中"导入多媒体资源"按钮为纯静态 `<button>`,无 `onClick` 事件和隐藏的 `<input type="file">`
|
||||||
|
|
||||||
|
### C. 解决方案
|
||||||
|
1. `websocket.ts` URL 改为 `ws://192.168.3.11:8000/ws/progress`
|
||||||
|
2. `backend/main.py` 添加 `@app.websocket("/ws/progress")` 路由 + `ConnectionManager` 连接管理
|
||||||
|
3. `pip install websockets` 为 Uvicorn 提供 WebSocket 协议支持
|
||||||
|
4. 后端 `Project.status` 默认值从 `"pending"` 改为 `"Ready"`
|
||||||
|
5. 前端增加 `'pending'` / `'Pending'` 状态分支显示"待处理"
|
||||||
|
6. 为导入按钮添加 `onClick` + `useRef<HTMLInputElement>` + 隐藏文件输入框,调用 `uploadMedia` API
|
||||||
|
|
||||||
|
### D. 后续如何避免问题
|
||||||
|
1. 任何涉及网络地址的配置(HTTP / WebSocket)必须同步修改,不能遗漏
|
||||||
|
2. 新增 WebSocket 功能时,同步检查 Uvicorn 是否安装了协议支持库
|
||||||
|
3. 前后端状态枚举值必须对齐,或后端返回时做映射转换
|
||||||
|
4. 所有按钮类 UI 元素必须绑定事件处理器,禁止纯装饰性按钮
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
> 新增经验请追加到文件末尾,保持时间倒序或正序均可,但需确保每条经验包含完整的 A/B/C/D 四段。
|
> 新增经验请追加到文件末尾,保持时间倒序或正序均可,但需确保每条经验包含完整的 A/B/C/D 四段。
|
||||||
|
|||||||
48
工程分析/需求分析-2026-04-29-22-49-38.md
Normal file
48
工程分析/需求分析-2026-04-29-22-49-38.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# 需求分析 - 2026-04-29-22-49-38
|
||||||
|
|
||||||
|
## 需求来源
|
||||||
|
- 提出时间: 2026-04-29-22-49-38
|
||||||
|
- 需求类型: 缺陷修复
|
||||||
|
|
||||||
|
## 原始需求描述
|
||||||
|
1. 项目库中视频显示"异常",且无法点击导入多媒体资源
|
||||||
|
2. 控制台 WebSocket 报错:
|
||||||
|
- `WebSocket connection to 'ws://localhost:8000/ws/progress' failed`
|
||||||
|
- `WebSocket is closed before the connection is established`
|
||||||
|
|
||||||
|
## 需求拆解
|
||||||
|
|
||||||
|
### 需求 1: 修复 WebSocket URL(localhost → IP)
|
||||||
|
- **详细描述**: `websocket.ts` 硬编码 `ws://localhost:8000/ws/progress`,浏览器解析为客户端本地地址导致连接失败
|
||||||
|
- **优先级**: P0-阻塞
|
||||||
|
- **影响范围**: `src/lib/websocket.ts`
|
||||||
|
- **验收标准**: WebSocket 连接到服务器实际 IP
|
||||||
|
|
||||||
|
### 需求 2: 后端添加 WebSocket 路由
|
||||||
|
- **详细描述**: FastAPI 后端未实现 `/ws/progress` WebSocket endpoint,前端连接被拒绝
|
||||||
|
- **优先级**: P0-阻塞
|
||||||
|
- **影响范围**: `backend/main.py`
|
||||||
|
- **验收标准**: 后端接受 WebSocket 连接,可推送解析进度消息
|
||||||
|
|
||||||
|
### 需求 3: 修复"导入多媒体资源"按钮点击
|
||||||
|
- **详细描述**: `ProjectLibrary.tsx` 中按钮为纯静态 UI,无 `onClick` 事件和文件输入框
|
||||||
|
- **优先级**: P0-阻塞
|
||||||
|
- **影响范围**: `src/components/ProjectLibrary.tsx`
|
||||||
|
- **验收标准**: 点击按钮弹出文件选择器,可选择视频/图片文件
|
||||||
|
|
||||||
|
### 需求 4: 修复项目状态显示"异常"
|
||||||
|
- **详细描述**: 后端数据库项目默认状态为 `'pending'`,前端只识别 `'Ready'` 和 `'Parsing'`,其他状态均显示"异常"
|
||||||
|
- **优先级**: P0-阻塞
|
||||||
|
- **影响范围**: `backend/models.py` 或 `src/components/ProjectLibrary.tsx`
|
||||||
|
- **验收标准**: 新建项目默认显示正确状态(非"异常")
|
||||||
|
|
||||||
|
## 约束条件
|
||||||
|
- 保持中文界面
|
||||||
|
- 最小修改原则
|
||||||
|
- 不破坏现有 API 契约
|
||||||
|
|
||||||
|
## 风险评估
|
||||||
|
| 风险点 | 影响 | 缓解措施 |
|
||||||
|
|--------|------|----------|
|
||||||
|
| WebSocket 实现复杂度高 | 中 | 先实现基础连接和心跳,进度推送后续完善 |
|
||||||
|
| 文件上传无后端对接 | 低 | 前端先实现文件选择器,调用已有 uploadMedia API |
|
||||||
Reference in New Issue
Block a user