# T-WEB-004 动作数据上传 + 来源字段 - **状态**:done - **依赖**:T-WEB-002 ## 目标 上传动作文件(BVH / VMD / FBX / glTF / GLB / keypoints-json),**强制填写来源五元组**。 ## load ``` DSH.md contracts/rest-api.yaml # 锚点:#/paths/~1api~1motions contracts/data-models.md # 锚点:§3 Motion contracts/model-formats.md # 锚点:§2 动作格式、§3 上传限制 web-server/src/motions/* web-server/src/storage/storage.service.ts web-server/src/preprocess/vmd-parser.ts # VMD 上传后的预处理链路 web-server/src/preprocess/mmd-skeleton.ts web-server/src/preprocess/mmd-pose.ts web-server/src/preprocess/retarget.ts # worldFramesToKeypointFrames() ``` ## 端点 | 方法 | 路径 | 说明 | |---|---|---| | GET | `/api/motions` | 分页 + `q`/`songId`/`format`/`status` 过滤 | | POST | `/api/motions` | multipart 上传 | | GET | `/api/motions/:id` | 详情 | | PATCH | `/api/motions/:id` | 更新元数据(含 `detachSong`) | | DELETE | `/api/motions/:id` | 软删除 | ## 存储策略(`contracts/model-formats.md` §3) ``` ${STORAGE_DIR}/{kind}/{yyyy}/{mm}/{sha256前2位}/{sha256}.{ext} ``` - 上传先落 `${STORAGE_DIR}/tmp`(multer diskStorage),避免大文件占用内存 - `StorageService` 流式计算 sha256;**相同内容只落盘一次**,复用 `assets` 记录 - 并发同文件上传靠 `assets.sha256` 唯一索引兜底,冲突后回退到已存在记录 - 路径永不使用原始文件名;`absolutePath()` 拒绝逃逸存储根目录 ## 校验 - 扩展名白名单:`.bvh .vmd .fbx .gltf .glb .json`,其他 → 400 - 来源五元组缺任一项 → 400 - `songId` 指向不存在的曲目 → 400 - 单文件上限 `UPLOAD_MAX_BYTES`(默认 512 MiB) ## VMD 支持(`.vmd`) - 管理端上传控件已支持 `.vmd`(accept、格式筛选、中文标签「VMD(MMD)」) - 入库后 `format=vmd`;预处理走内置链路,不依赖 Assimp: `vmd-parser.ts`(二进制 + Shift-JIS + 贝塞尔插值)→ `mmd-skeleton.ts`(标准骨架) → `mmd-pose.ts`(FK + 腿部 IK + Z 镜像)→ `retarget.ts` 的 `worldFramesToKeypointFrames()`(与 BVH 共用骨盆归零 / 自适应缩放 / 投影 / 关节角) - 固定 30 fps;腿部按 `足IK` 是否带位移自动选 IK 或 FK - 上传后处理失败与 BVH 一致:写入 `preprocessState=failed` 与可读错误 ## 验收 ```bash npm run test:smoke ``` 冒烟 [2] 段落:上传 201、自动识别 `format=bvh`、`fileSize` 记录、缺来源 400、 `evil.exe` 400。 VMD 实测(用 `../示例文件/` 下的两个样本): | 样本 | 帧数 | 腿部驱动 | 结果 | |---|---|---|---| | `【动作】霜雪千年.vmd`(0.79 MiB) | 2901 | `足IK` 驱动(IK) | 0 NaN / 0 越界,预处理约 0.4s | | `恋爱循环.vmd`(20.24 MiB) | 4604 | 烘焙 FK | 0 NaN / 0 越界,预处理约 2.5s | 上传前可先用诊断工具自查(只读): ```bash node scripts/inspect-vmd.mjs 动作.vmd [--frames N] [--json out.json] ```