Files
opendance-web/tasks/017-model-unusable-reason.md
zijunchiang 12041f2e58 fix(upload,models): 上传落盘补齐兜底 + 不可用模型归因准确(v0.7.5)
两个真实缺陷,都由本轮的「跨端边界验收」暴露:

一、上传落盘:去重命中时删掉了唯一副本(T-WEB-017)
persistTempFile(动作 / 模型 / 场景上传走的通路)在 sha256 去重命中时直接
safeUnlink(tempPath) 并返回旧记录 —— 一旦记录指向的路径不存在(换过 STORAGE_DIR、
只恢复数据库、手工删过文件),上传仍返回 201,但后续动作预处理 500
(实测 ENOENT .../<sha>.vmd)、模型分析与缩略图静默失败。持久化通路 persistBuffer
早在上一轮就补了「文件缺失则按原路径补写」,这条通路漏了;现在同样处理:
先 existsSync(absolutePath(existing)),缺失则按记录里的原路径 rename(跨设备退化为
copyFile)补写,只有确认目标存在才删临时文件。
新增 test/storage-persist.spec.ts(5 项):首次上传 / 去重命中且文件在 /
去重命中但文件已丢失(补写而非删除)/ persistBuffer 回归 / 空文件拒绝。
反向验证:临时还原修复后第 3 条立刻失败,恢复后 5 项全过。

二、不可用模型的原因归因不准确(T-WEB-017)
unusableReason() 只要 stats 里有 humanoid 数字就归因于 humanoid,于是一个直接上传的
.glb(骨架完全合规、drivable=true、只是没有 VRM 容器)会被写成
「humanoid 不达标(19/22)」,把审核引向"重新绑定骨骼"而不是"以 VRM 交付"。
实测(跨端验收用真实 PMX→Assimp→GLB,886 KB):
  修复前 → humanoid 不达标(19/22),未产出 VRM
  修复后 → 未产出 VRM(格式为 GLB;humanoid 覆盖 19/22 已达标),需重新以 VRM 交付才能进入客户端目录
现在按 drivable 区分:不满足 16 根必需骨才归因 humanoid;达标但没 VRM 容器则明确说"已达标"。
新增 2 项单测(model-usability 5 → 7 项)。

三、跨端边界验收(Windows × Web)新增两类场景,双方都断言
  * 不可用模型:真实 PMX 用 Assimp 转 GLB 后入库(等价"转换只产出 GLB")→
    usable=false、原因准确、**不进入 catalog/export**、defaultModelId 不变;
    包内客户端的 bundle.models 里没有任何非 VRM 模型;
  * 场景 config 只给 {physics:{gravity:{y:-9.8}}} → 服务端补齐默认
    (碰撞体 auto、地面 0.6/45°、静风、相机 follow、环境光 1、出生点 1 条),
    客户端解析后逐项一致。

实测:npm run verify 全绿(契约 6 项 / 35 路由 / 版本 0.7.5 / 27 文件 309 项测试 /
build / 产物级实体注册核对 / 启动冒烟);跨端集成验收在**发布包起的服务端**上全部通过。
版本 0.7.4 → 0.7.5。
2026-09-18 03:03:50 +08:00

73 lines
3.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# T-WEB-017:不可用模型的原因归因必须准确
- **状态**: done
- **依赖**: T-WEB-012(模型一律转 VRM)
- **文档**: v1.6 §4.9 / §7.6
- **契约**: `model-formats 0.5.0`、`data-models 0.7.0`
## 缺陷
`unusableReason()` 原来只要 `stats` 里有 humanoid 数字就归因于 humanoid:
```ts
if (stats && typeof stats.humanoidMapped === 'number' && typeof stats.humanoidTotal === 'number') {
return `humanoid 不达标(${stats.humanoidMapped}/${stats.humanoidTotal}),未产出 VRM`;
}
```
于是一个**直接上传的 `.glb`**(骨架完全合规、`drivable = true`、只是没有 VRM 容器)
会被写成「**humanoid 不达标(19/22)**」—— 审核界面据此会把修复方向引到
「重新绑定骨骼」,而实际要做的是「以 VRM 交付」。
## 修复
按 **`drivable`** 区分两种不可用:
| 情形 | 原因文案 |
|--|--|
| `drivable !== true`(确实不满足 16 根必需骨) | `humanoid 不达标(m/22),未产出 VRM` |
| `drivable === true` 但没有 VRM 容器(直接上传的 GLB / 转换只产出 GLB) | `未产出 VRM(格式为 GLB;humanoid 覆盖 m/22 已达标),需重新以 VRM 交付才能进入客户端目录` |
| 没有 stats(分析失败等) | `未产出 VRM(格式为 GLB),不可用于客户端`(不变) |
新增两条单测(`test/model-usability.spec.ts`,5 → 7 项):
「humanoid 已达标时**不得**出现『humanoid 不达标』字样」与「`drivable=false` 时仍归因于 humanoid」。
## 顺带修掉:上传落盘的对称兜底(`persistTempFile`)
做跨端边界验收时,动作预处理在**换了 `STORAGE_DIR`** 的服务端上直接 500:
```
ERROR [PreprocessService] 动作 #7 预处理失败:ENOENT ... /web075/storage/motion/2026/09/a8/<sha>.vmd
```
根因:`persistTempFile`(动作 / 模型 / 场景上传走的通路)在去重命中时
**直接 `safeUnlink(tempPath)` 删掉刚上传的唯一副本**并返回旧记录;一旦记录指向的路径
不存在(换过 `STORAGE_DIR`、只恢复数据库、手工删过文件),上传仍返回 **201**,
但后续动作预处理 500、模型分析与缩略图静默失败。`persistBuffer` 早在上一轮就补了
「文件缺失则按原路径补写」,这条通路漏了。
**修复**:去重命中时先 `existsSync(absolutePath(existing))`;缺失则按记录里的原路径
`rename`(跨设备退化为 `copyFile`)补写,只有确认目标存在才删临时文件。
新增 `test/storage-persist.spec.ts`(5 项):首次上传 / 去重命中且文件在 /
**去重命中但文件已丢失(补写而非删除)** / `persistBuffer` 回归 / 空文件拒绝。
**反向验证**:临时还原修复后,第 3 条用例立即失败(`existsSync(absolute) === false`),
恢复后 5 项全过。
## 跨端边界验收(Windows × Web)
`accept-integration.sh` 的播种脚本新增两个**边界**场景,并用发布包里的客户端断言:
1. **不可用模型**:把真实 PMX 用 Assimp 转成 GLB(等价于"转换只产出 GLB"的入库形态),
上传并审核通过(**不**设为默认模型),断言
* `/api/models/{id}` 的 `usable === false` 且 `unusableReason` 不谎报 humanoid;
* 该模型**不出现在** `GET /api/catalog/export`;
* `defaultModelId` 仍是可用的 VRM 模型;
* 包内客户端的 `bundle.models` 里**没有**任何非 VRM 模型(① 的消费侧形态)。
2. **场景 config 缺字段**:只给 `physics.gravity`,断言服务端补齐默认值
(碰撞体 `auto`、地面默认摩擦/坡度、静默风、相机 `follow`…),客户端解析后同样一致。
## 实测
见 `build/accept-integration.sh` 输出(需要已部署的 Web 服务端)。