2026-04-29-22-49-38 - 修复WebSocket连接+项目状态异常+导入按钮无响应+后端WS路由

This commit is contained in:
2026-04-29 22:56:22 +08:00
parent 0f7b1ec31d
commit a4fb655841
8 changed files with 269 additions and 8 deletions

View File

@@ -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)

View File

@@ -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()

View File

@@ -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" /> </>
)} )}

View File

@@ -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;
} }

View 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 分钟

View 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

View File

@@ -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 四段。

View 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 URLlocalhost → 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 |