Files
opendance-web/tasks/007-upload-review.md
T
opendance 9eae6f3e43 feat(models): 骨骼重绑定——修复 MMD 模型 humanoid 恒为 0/22(契约 skeleton-topology 0.2.0)
根因:normalizeBoneName() 只保留 a-z0-9,**中日文字符被整个丢掉**:
'上半身'/'左ひじ'/'首' 归一化后都是空串,MMD 模型的骨名必然失配,
humanoid 覆盖率恒为 0/22 -> 不可驱动 -> 只能回退交付 GLB 并标记不可用。

修复:
1. 归一化保留 CJK/平假名/片假名/长音符/々,并把全角 ASCII(足IK)折成半角;
2. 候选表补齐四类命名(22 根 x 8-13 个候选):VRoid / Mixamo /
   MMD 标准骨架(下半身/上半身/上半身2/首/頭/左肩/左腕/左ひじ/左手首/
   左足/左ひざ/左足首/左つま先)/ 常见中文名(臀部/头/脖子/左大臂/左大腿…);
   精确匹配保证「左足IK」不会抢占「左足」;
3. Windows 客户端 humanoid_map.gd 的 CANDIDATES 由服务端表**生成写入**,
   normalize() 同步改造,parity 测试断言键/候选顺序完全一致;
4. 契约 skeleton-topology -> 0.2.0:新增 §6「内部动作标准与模型骨骼重绑定」
   (三层标准、22/16 根骨、归一化与匹配规则、实测记录);
5. 新增 4 项测试;规模 17 文件 / 229 项。

实测:奥黛塔 PMX 0/22 -> 21/22、初音未来 PMX 0/22 -> 19/22,均 drivable=true 且产出 VRM。
版本 0.4.0。
2026-09-17 00:10:07 +08:00

105 lines
5.7 KiB
Markdown

# T-WEB-007 上传审核流程
- **状态**: done(v0.2.0 追加:模型审核面板 / 骨骼详细界面 / `setPrimary`)
- **依赖**: T-WEB-003 / 004 / 005
## 目标
对歌曲、动作、模型三类资产实施统一的状态机与审核流水,确保只有审核通过的资产
被客户端消费。模型上传**一律 `pending`**,审核是模型可见与成为默认模型的唯一入口。
## load
```
DSH.md
contracts/rest-api.yaml # 锚点:#/paths/~1api~1reviews
contracts/data-models.md # 锚点:1.2 PublishStatus、4.1 默认模型选择规则、5 ReviewRecord
contracts/database.md # 锚点:状态机
web-server/src/reviews/*
web-server/src/models/model-inspector.ts
web-server/admin/src/components/ModelReviewPanel.tsx
web-server/admin/src/lib/bone-tree.ts
```
## 状态机(契约冻结)
```
draft ──submit──▶ pending ──approve──▶ published
│
└──reject──▶ rejected ──submit──▶ pending
```
| 方法 | 路径 | 说明 |
|--|--|--|
| GET | `/api/reviews` | 不传 `status` -> 审核流水;传 `status` -> 该状态待审资产(三类合并) |
| GET | `/api/reviews/:targetType/:id/history` | 单个资产的审核历史 |
| POST | `/api/reviews/:targetType/:id/submit` | -> `pending` |
| POST | `/api/reviews/:targetType/:id/approve` | -> `published`; **模型**可带 `setPrimary:true` |
| POST | `/api/reviews/:targetType/:id/reject` | -> `rejected`, **必须填写意见** |
| POST | `/api/reviews/:targetType/:id/withdraw` | -> `draft` |
## 关键实现
- `STATUS_TRANSITIONS` 是状态机的唯一来源;非法流转一律 **409**,
重复审核同一状态同样 409。
- 驳回未填 `comment` -> **400**(合规要求:拒绝侵权内容需留痕)。
- 每次成功流转都写一条 `review_records`,记录 `fromStatus` / `toStatus` /
`reviewer` / `comment`。
- 三类资产的直连 PATCH 也会校验状态机,避免绕过审核流程。
- `/api/catalog/export` 与 `/api/catalog/version` 只导出 `published`。
- **模型上传只能 pending**: `ModelsService.create()` 固定
`status=PublishStatus.Pending` + `isPrimary=false`,请求体里的 `autoDefault`
等旁路一律不生效(硬性约束,见 DSH.md 6.9)。
## 模型审核面板(v0.2.0)
待审列表只有 `PendingItem`(不含模型详情),面板用 `item.targetId` 现拉
`GET /api/models/:id`(`admin/src/components/ModelReviewPanel.tsx`):
| 区域 | 内容 |
|--|--|
| 基本 | 缩略图、格式、状态、文件地址、转换说明 |
| 指标 | 体积(`fileSize`)/ 格式 / 容器 / VRM 版本 / 面数 / 顶点 / 网格 / 材质 / 贴图 / 骨骼 / 节点 / 形变目标 / 动画 / 图元 / 蒙皮 / 骨骼最大层级 |
| 可驱动性 | 结论横幅:16 根必需 humanoid 骨齐全 -> 可驱动;不全 -> 列出缺哪些(直接标红);非 glTF 容器 -> 只能审核来源与体积 |
| humanoid 覆盖 | 已映射 / 总数(22)+ 进度条 + 已命中规范名徽章明细 |
| warnings | `ModelStats.warnings` 逐条列出 |
| 骨骼详细界面 | 扁平骨骼树按 `depth` 缩进(14px/层),显示骨骼名、是否蒙皮徽章、humanoid 映射徽章、子节点数;支持按名搜索(保留祖先链)、「仅显示蒙皮骨骼」开关、限高滚动 + **只渲染前 400 行**(超出提示还有多少根),千根骨骼不卡 |
| stats 为空 | 提示"暂无分析数据(旧数据或后端尚未完成分析)",只能按格式/体积/来源判断 |
### 通过 / 驳回 / 设为默认
- 面板底部勾选「同时设为全局默认模型」-> 点「通过」时请求体带 `setPrimary:true`:
`ReviewsService.transition()` 保存模型为 `published` 后调用
`ModelsService.setPrimary(id)`(同一事务清其余 `is_primary`),并把身份写进源码
`src/config/default-model.ts`。
- 单独设置走 `POST /api/models/:id/primary`: **仅 `published` 允许,否则 409**。
- 驳回必须填意见(400)。
### 骨骼树过滤(`admin/src/lib/bone-tree.ts`,纯函数、可单测)
- `filterBoneTree(tree, keyword, onlySkin)`:先按关键字(大小写不敏感)与 `onlySkin`
取命中集合,再沿父链补齐**祖先链**,保证按 `depth` 缩进的树不断裂;空关键字 +
非 `onlySkin` 时直接返回副本。
- `boneTreeStats(tree)`:总数 / 蒙皮数 / 已映射 humanoid 数 / 最大层级。
## 验收
```bash
cd web-server
npx vitest run # 17 个文件 / 229 项通过(含 admin-bone-filter 8 项、.env 预检 12 项、Assimp 13 项、缩略图贴图 6 项、MMD 材质对齐 9 项、模型可用性 5 项、MMD 骨名重绑定 4 项)
npm run test:smoke
```
冒烟 [5] 段落:submit -> pending、待审列表包含、驳回无意见 400、approve -> published、
重复审核 409、流水恰好 2 条。
| 验收项 | 状态 |
|--|--|
| 模型上传只能是 pending | ✅ `create()` 固定 `PublishStatus.Pending`,不自动发布/设默认 |
| 模型审核面板展示指标 + 分析 | ✅ 体积/格式/容器/VRM 版本/面数/顶点/网格/材质/贴图/骨骼/节点/形变目标/动画 |
| 骨骼详细界面与过滤 | ✅ 缩进 + 蒙皮/映射徽章 + 搜索保留祖先链 + 仅蒙皮开关 + 限高 + 前 400 行(`admin-bone-filter.spec.ts` 8 项) |
| `setPrimary` 随审核通过生效 | ✅ `POST /api/reviews/model/22/approve` + `setPrimary:true` -> 200、`status=published`、`isPrimary=true`,源码种子写入 `sourceSha256=72eb6882...` |
| 单独设默认仅 published | ✅ `POST /api/models/:id/primary` 非 published -> 409 |
| 驳回必须填意见 | ✅ 无 `comment` -> 400 |
| 目录重建后默认模型 | ✅ `GET /api/catalog/export` 的 `defaultModelId` 由 VRM 变为 **22**(库中另有 2 个已发布 VRM) |