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 模型”页签。