diff --git a/WebSite/server.ts b/WebSite/server.ts index ecff4ae..b31dddf 100644 --- a/WebSite/server.ts +++ b/WebSite/server.ts @@ -2595,6 +2595,12 @@ async function startServer() { writeState(state); res.json(project); } catch (error) { + console.error('[import-assets] failed', { + projectId: req.params.projectId, + kind, + fileCount: multerFiles.length + legacyUploadedFiles.length, + message: error instanceof Error ? error.message : error, + }); res.status(422).json({ message: error instanceof Error ? error.message : '项目资产导入失败' }); } finally { cleanupUploadedTempFiles(multerFiles); diff --git a/WebSite/src/components/ProjectLibrary.tsx b/WebSite/src/components/ProjectLibrary.tsx index 0adc23a..3a50e61 100644 --- a/WebSite/src/components/ProjectLibrary.tsx +++ b/WebSite/src/components/ProjectLibrary.tsx @@ -135,7 +135,8 @@ interface AssetImportProgressState { totalBytes: number; loadedBytes: number; percent: number; - phase: 'uploading' | 'processing' | 'done'; + phase: 'uploading' | 'processing' | 'done' | 'failed'; + message?: string; } function formatFileSize(value: number) { @@ -953,8 +954,17 @@ export default function ProjectLibrary({ setActionMessage(kind === 'dicom' ? `已导入 ${updated.dicomCount} 张 DICOM 影像` : `已导入 ${updated.modelCount ?? 0} 个 STL 模型`); window.setTimeout(() => setAssetImportProgress(null), 1800); } catch (error) { - setActionMessage(error instanceof Error ? error.message : '项目资产导入失败'); - window.setTimeout(() => setAssetImportProgress(null), 2400); + const message = error instanceof Error ? error.message : '项目资产导入失败'; + setAssetImportProgress((current) => ({ + kind, + fileCount: files.length, + totalBytes: current?.totalBytes ?? totalBytes, + loadedBytes: current?.loadedBytes ?? 0, + percent: current?.percent ?? 0, + phase: 'failed', + message, + })); + setActionMessage(message); } finally { setAssetImporting(false); } @@ -1516,15 +1526,29 @@ export default function ProjectLibrary({ {assetImportProgress && ( -
+
-
- +
+ {assetImportProgress.phase === 'failed' ? : }

- {assetImportProgress.phase === 'done' + {assetImportProgress.phase === 'failed' + ? `${describeImportKind(assetImportProgress.kind)}导入失败` + : assetImportProgress.phase === 'done' ? `${describeImportKind(assetImportProgress.kind)}导入完成` : assetImportProgress.phase === 'processing' ? '上传完成,服务器正在解压与解析' @@ -1533,15 +1557,32 @@ export default function ProjectLibrary({

{assetImportProgress.fileCount} 个文件 · {formatFileSize(assetImportProgress.loadedBytes)} / {formatFileSize(assetImportProgress.totalBytes)}

+ {assetImportProgress.phase === 'failed' && assetImportProgress.message && ( +

+ {assetImportProgress.message} +

+ )}
- + {assetImportProgress.percent}%
diff --git a/工程分析/实现方案-2026-05-21-00-43-44.md b/工程分析/实现方案-2026-05-21-00-43-44.md new file mode 100644 index 0000000..cae6511 --- /dev/null +++ b/工程分析/实现方案-2026-05-21-00-43-44.md @@ -0,0 +1,51 @@ +# 实现方案-2026-05-21-00-43-44 + +## 实现方案文档路径 + +`工程分析/实现方案-2026-05-21-00-43-44.md` + +## 修改目标 + +- 复现 `3279-STL.zip` 导入时的 422 错误,获取具体失败原因。 +- 修复 ZIP 内 STL 解压/筛选/写入逻辑,使该压缩包可导入。 +- 优化导入失败 UI,将错误状态和原因显示在顶部导入进度区域。 +- 补充测试记录和经验记录。 + +## 涉及路径 + +- `WebSite/server.ts` +- `WebSite/src/components/ProjectLibrary.tsx` +- `工程分析/经验记录.md` + +## 技术路线 + +1. 使用 `curl -F files=@3279-STL.zip` 对现有接口复现错误,查看响应体。 +2. 检查 `AdmZip` 解包结果、ZIP 条目大小和写入逻辑。 +3. 如果问题来自大 ZIP 一次性 `entry.getData()` 导致内存或写入失败,则改为按条目流式/逐条写入目标目录,减少内存峰值。 +4. 如果问题来自文件名扩展名、编码或路径处理,则修正筛选和安全文件名逻辑。 +5. 前端导入状态增加 `failed` phase,错误时保留进度卡并展示错误文案。 + +## 执行步骤 + +1. 创建本次工程分析文档。 +2. 本地复现 422,记录响应。 +3. 修改后端 ZIP 导入逻辑和前端错误反馈。 +4. 执行 `npm run lint`、`npm run build`。 +5. 用 `3279-STL.zip` 对临时项目实际导入验证。 +6. 追加经验记录,提交推送 Gitea,重新部署。 + +## 兼容性与回滚方案 + +- 若 `AdmZip` 对该 ZIP 仍不稳定,可增加系统 `unzip` fallback 或改用更适合流式解压的库。 +- 前端错误卡只影响导入反馈,不影响其他页面。 +- 导入失败时不更新项目状态,避免半成品资产进入项目库。 + +## 预计文件变更 + +- 后端 1 个、前端组件 1 个、工程分析文档 4 个。 + +## 提交与部署策略 + +- 不提交 `3279-STL.zip` 和解压资产。 +- commit message 包含 `2026-05-21-00-43-44` 与“修复 ZIP STL 导入失败”。 +- 部署后验证 `/api/health`、首页以及 ZIP 导入结果。 diff --git a/工程分析/测试方案-2026-05-21-00-43-44.md b/工程分析/测试方案-2026-05-21-00-43-44.md new file mode 100644 index 0000000..af6119c --- /dev/null +++ b/工程分析/测试方案-2026-05-21-00-43-44.md @@ -0,0 +1,51 @@ +# 测试方案-2026-05-21-00-43-44 + +## 测试方案文档路径 + +`工程分析/测试方案-2026-05-21-00-43-44.md` + +## 静态检查 + +- 执行 `cd WebSite && npm run lint`,确认 TypeScript 类型检查通过。 +- 使用 `rg` 检查导入错误反馈和 ZIP 处理路径。 + +执行结果:`npm run lint` 已通过。 + +## 构建检查 + +- 执行 `cd WebSite && npm run build`,确认生产构建通过。 + +执行结果:`npm run build` 已通过;Vite 仅提示既有 chunk 体积超过 500 kB。 + +## 关键业务场景验证 + +- 使用 `3279-STL.zip` 调用项目导入接口,确认返回 200 且 `modelCount` 与 ZIP 内 STL 数量一致。 +- 前端导入失败时顶部进度区域显示失败状态和错误原因。 +- 前端导入成功时顶部进度区域显示完成状态。 + +执行结果:使用 `3279-STL.zip` 对项目 `project-mpea0n3e-akbpn` 导入成功,接口返回 200,`modelCount: 26`,`stlFiles` 已包含 ZIP 内 26 个 STL。 + +## 医学影像数据相关边界验证 + +- ZIP 中所有 `.stl` 文件写入项目级 `WebSite/data/uploads//STL`。 +- 导入后 `project.stlFiles` 包含 ZIP 内模型文件名。 +- 导入失败不会污染默认 `Head_CT_ReConstruct/`。 +- 临时测试项目和上传目录验证后清理。 + +## 部署验证 + +- 重启 `tmux` 会话 `revoxelseg-dicom`。 +- 验证: + - `curl http://127.0.0.1:4000/api/health` + - `curl -I http://127.0.0.1:4000/` + +## Git/Gitea 备份验证 + +- `git status --short` 确认暂存范围不包含 `3279-STL.zip`、解压模型或无关历史删除。 +- commit message 包含 `2026-05-21-00-43-44`。 +- 推送 Gitea 成功。 + +## 风险与回归关注点 + +- ZIP 内大型 STL 不应同时全部进入内存。 +- 前端错误信息不能只显示在左侧项目栏底部,应在导入进度卡上直接可见。 diff --git a/工程分析/经验记录.md b/工程分析/经验记录.md index 6d91aab..72c3e1a 100644 --- a/工程分析/经验记录.md +++ b/工程分析/经验记录.md @@ -1423,3 +1423,21 @@ C. 解决问题方案 D. 后续如何避免问题 凡是医学影像、STL、NIfTI、压缩包等大文件导入,都不要在浏览器内转 base64 或拼接巨型 JSON;默认使用 multipart 或分片上传,并把解压、筛选、校验放在服务端。导入 UI 应区分“上传进度”和“服务器处理阶段”,避免用户误以为进度到 100% 就已经完成解析。 + +## 2026-05-21-00-43-44 导入失败要在主操作区保留可见错误 + +A. 具体问题 + +用户导入 `3279-STL.zip` 后浏览器控制台出现 `422 Unprocessable Entity`,但页面主操作区没有明显反馈,用户感知为“没有反应”。后续用同一 ZIP 直接调用接口验证可成功导入 26 个 STL,说明压缩包本体可读,问题更集中在失败反馈不可见和缺少后端可追踪日志。 + +B. 产生问题原因 + +前端导入异常只写入左侧项目栏底部的小号 `actionMessage`,主内容区域的进度条会消失,用户视线集中在右侧模型区域时很难看到错误。后端 422 返回虽然包含 `message`,但服务端没有记录项目、导入类型和文件数量,排查需要重新手工复现。 + +C. 解决问题方案 + +将项目库导入状态扩展为 `uploading/processing/done/failed`,失败时在顶部进度卡保留红色错误状态、百分比和后端错误原因,不再自动消失。后端导入 catch 中增加结构化日志,记录 `projectId`、`kind`、上传文件数量和错误消息。并用用户提供的 `3279-STL.zip` 对项目 `123` 实际导入,确认项目更新为 26 个 STL。 + +D. 后续如何避免问题 + +所有文件导入、导出、保存这类长操作失败时,都要在主操作路径旁保留显眼错误卡,不能只写入边栏或短暂 toast。后端对 4xx/5xx 的可恢复业务错误应记录足够上下文,尤其是项目 ID、导入类型、文件数量和原始异常消息,方便从用户截图快速定位。 diff --git a/工程分析/需求分析-2026-05-21-00-43-44.md b/工程分析/需求分析-2026-05-21-00-43-44.md new file mode 100644 index 0000000..b20a44c --- /dev/null +++ b/工程分析/需求分析-2026-05-21-00-43-44.md @@ -0,0 +1,46 @@ +# 需求分析-2026-05-21-00-43-44 + +## 开始时间 + +2026-05-21-00-43-44 + +## 原始需求摘要 + +1. 用户上传 `3279-STL.zip` 后项目库没有明显反应。 +2. 浏览器控制台显示 `/api/projects/:projectId/import-assets` 返回 `422 Unprocessable Entity`。 +3. 用户已将 `3279-STL.zip` 放在当前项目目录,需要定位压缩包导入失败原因并修复。 + +## 业务目标 + +- 让包含多个 STL 的 ZIP 压缩包可以稳定导入项目库 3D 模型。 +- 导入失败时前端必须给出显眼、可读的错误反馈,而不是让用户感觉“没有反应”。 +- 后端返回具体错误原因,便于定位压缩包、模型文件或服务器处理问题。 + +## 输入与输出 + +- 输入:项目根目录中的 `3279-STL.zip`,项目库 3D 模型导入动作。 +- 输出:成功导入 ZIP 内 STL 模型并更新项目 `modelCount/stlFiles`,或在失败时展示明确原因。 + +## 影响范围 + +- `WebSite/server.ts`:ZIP 解压、文件名处理、STL 筛选、错误信息。 +- `WebSite/src/components/ProjectLibrary.tsx`:导入错误提示可见性、导入进度失败状态。 +- `WebSite/src/lib/api.ts`:错误消息解析。 +- `工程分析/经验记录.md`:记录本次压缩包导入经验。 + +## 关键约束 + +- 不把 `3279-STL.zip` 或解压后的大模型资产纳入 Git 提交。 +- 继续保证导入只写入项目级上传目录,不影响默认演示数据。 +- 不回退到浏览器 base64 导入。 + +## 风险点 + +- ZIP 内 STL 总体积较大,服务端一次性解压到内存可能产生压力。 +- ZIP 文件名或条目可能存在编码、路径、目录层级差异。 +- 导入成功后项目库预览如果一次性加载过多大 STL,也可能造成浏览器压力,需要至少保证导入链路稳定。 + +## 默认假设 + +- 当前 422 由后端解压或写入阶段抛错造成,而不是项目不存在。 +- 用户截图中的 ZIP 内条目均为 `.stl`,导入目标处于“3D 模型”页签。