From c22790f3fe1c87d4d1a46aebc1a6c0b1f62c1f4b Mon Sep 17 00:00:00 2001 From: zijunchiang Date: Tue, 15 Sep 2026 19:36:42 +0800 Subject: [PATCH] =?UTF-8?q?chore(contracts):=20=E5=90=8C=E6=AD=A5=E5=A5=91?= =?UTF-8?q?=E7=BA=A6=EF=BC=88=E9=BB=98=E8=AE=A4=E6=A8=A1=E5=9E=8B=E6=94=B9?= =?UTF-8?q?=E7=94=A8=E5=8E=8B=E7=BC=A9=E5=8C=85=20sha256=20=E5=8C=B9?= =?UTF-8?q?=E9=85=8D=EF=BC=8C=E8=A1=A5=20sourceSha256=20=E4=B8=8E=E7=A1=AE?= =?UTF-8?q?=E5=AE=9A=E6=80=A7=E8=AF=B4=E6=98=8E=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/data-models.md | 8 ++++++-- contracts/model-formats.md | 1 + contracts/rest-api.yaml | 10 ++++++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/contracts/data-models.md b/contracts/data-models.md index da64438..ea0167d 100644 --- a/contracts/data-models.md +++ b/contracts/data-models.md @@ -103,7 +103,8 @@ rejected 已驳回 | `fileSize` | number | 读 | | | `thumbnailUrl` | string\|null | 读 | | | `conversionNote` | string\|null | 读 | 由压缩包转换而来时的说明(`Hatsune Miku.pmx → GLB,已内嵌 2 张贴图`);直传模型为 null | -| `fileSha256` | string\|null | 读 | 模型文件(转换产物)的 sha256,用于换库后定位默认模型 | +| `fileSha256` | string\|null | 读 | 模型文件(转换产物)的 sha256 —— **每次转换都会变**,仅作记录 | +| `sourceSha256` | string\|null | 读 | 上传压缩包的 sha256(稳定);默认模型匹配以它为准,非压缩包上传为 null | | `source*` | | ✅ | 来源五元组 | | `status` | `PublishStatus` | | | | `createdAt` / `updatedAt` | string | 读 | | @@ -115,7 +116,10 @@ rejected 已驳回 ``` 0. 环境变量 DEFAULT_MODEL_ID 指定,且该模型存在且已发布 1. 源码内写死的默认模型种子(web-server/src/config/default-model.ts 的 - DEFAULT_MODEL_SEED):按转换产物 sha256 匹配已发布模型 —— 换库/重建库后仍然有效 + DEFAULT_MODEL_SEED):按 **sourceSha256(上传压缩包的 sha256)** 匹配已发布模型, + 换库/重建库后仍然有效;没有压缩包时退回按产物 sha256,最后按名称兜底。 + ⚠ 不能用产物 sha256 当主键:实测 Assimp 的 GLB 导出不是字节级确定的, + 同一份 zip 每次转换得到的文件 sha256 都不同(BIN chunk 有十几字节浮动) 2. models 中 format = 'vrm' 且 status = 'published' 且 is_primary = 1 3. models 中 format = 'vrm' 且 status = 'published',按 id 升序取第一个 4. models 中 status = 'published',按 (format='vrm' DESC, id ASC) 取第一个 diff --git a/contracts/model-formats.md b/contracts/model-formats.md index 73c639e..36af55c 100644 --- a/contracts/model-formats.md +++ b/contracts/model-formats.md @@ -23,6 +23,7 @@ | 贴图 | GLB 产物里 Assimp 会把贴图写成外部 URI(还带 `\` 反斜杠),服务端把压缩包内的贴图**内嵌**进 GLB 的二进制块,保证单文件可加载 | | 溯源 | 压缩包原件以 `model-archive` 资产留存,写入 `models.source_asset_id` | | 默认模型 | 转换成功且 `autoDefault` 不为 false:`is_primary=1` + `status=published`,并把身份写进源码 `web-server/src/config/default-model.ts`,见 `data-models.md` §4.1 | +| 确定性 | **转换产物不是字节级确定的**:同一份 zip 连续转换三次得到三个不同的 sha256(大小相同,BIN chunk 有十几字节浮动)。因此默认模型种子以**压缩包 sha256** 为匹配键,产物 sha256 只作记录 | | 失败 | 压缩包里没有 `.pmx`、Assimp 不可用、两种格式都失败 → **HTTP 422**,不产生模型记录 | 安全:解压前逐条校验路径,拒绝绝对路径与 `..`(zip-slip);拒绝加密条目;只支持 STORE / DEFLATE。 diff --git a/contracts/rest-api.yaml b/contracts/rest-api.yaml index 6d9d835..0bbe2bb 100644 --- a/contracts/rest-api.yaml +++ b/contracts/rest-api.yaml @@ -331,8 +331,10 @@ paths: 压缩包原件作为 `model-archive` 资产留存以便溯源。 转换成功且 `autoDefault` 不为 false 时,该模型会被置为全局默认模型 - (`isPrimary=1` + `status=published`),并把身份(名称/格式/产物 sha256) + (`isPrimary=1` + `status=published`),并把身份(名称/格式/**压缩包 sha256**) 写进源码 `web-server/src/config/default-model.ts`。 + 注意:**转换产物不是字节级确定的**(同一份 zip 每次转换的产物 sha256 都不同), + 所以默认模型种子以压缩包 sha256 为匹配键,产物 sha256 只作记录。 requestBody: required: true content: @@ -774,7 +776,11 @@ components: fileSha256: type: string nullable: true - description: 模型文件(转换产物)的 sha256,用于换库后定位默认模型 + description: 模型文件(转换产物)的 sha256 —— 每次转换都会变(Assimp 导出非字节级确定),仅作记录 + sourceSha256: + type: string + nullable: true + description: 上传压缩包的 sha256(稳定);默认模型匹配以它为准,非压缩包上传为 null status: { $ref: '#/components/schemas/PublishStatus' } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time }