fix(models): 默认模型改用「压缩包 sha256」匹配(转换产物非字节级确定)

实测发现:同一份 zip 连续转换三次得到三个不同的产物 sha256(大小都是 1,152,464
字节,差异集中在 BIN chunk 的十几字节),说明 Assimp 的 GLB 导出不是字节级确定的。
因此原先把「转换产物 sha256」当作默认模型匹配键的设计是错的——换一次转换就失效。

- default-model.ts / default-model-source.ts:种子拆成 sourceSha256(上传压缩包的
  sha256,稳定,主匹配键)与 fileSha256(产物 sha256,仅作记录)
- models.service.ts:resolveDefaultModelId 依次按 压缩包 sha256 → 产物 sha256 →
  名称 匹配,都落空才回落到 VRM 优先规则并告警
- ModelDto 增加 sourceSha256(非压缩包上传为 null)
- scripts/import-default-model.mjs:写种子时用 sourceSha256;顺带修掉位置参数解析
  (文件当第一个参数时识别不出,会打印用法)
- 契约与文档同步:data-models §4.1、model-formats §1.1 记录该确定性结论;
  README / DSH / tasks/005 的「按产物 sha256 匹配」表述已更正

另附:把模型格式能力的实测矩阵写进 Release 说明(原生只支持 glTF 系;
PMX→FBX 失败;VRM→GLB 丢 VRM 扩展;humanoid 骨骼命名覆盖率)。

验证:125 项单测通过;清理掉两个"文件只在本机临时目录"的模型(避免 sha256 去重
导致后续上传不落盘);种子按压缩包 sha256 命中后 defaultModelId 正确解析。
This commit is contained in:
2026-09-15 19:36:37 +08:00
parent e4351c5062
commit ccced34dd6
12 changed files with 164 additions and 48 deletions
+36 -5
View File
@@ -41,8 +41,33 @@ const flag = (name, fallback = undefined) => {
};
const has = (name) => args.includes(name);
const positional = args.find((a) => !a.startsWith('--') && args[args.indexOf(a) - 1]?.startsWith('--') === false);
const file = positional;
/** 需要跟一个取值的开关(其余 --x 视为布尔开关) */
const VALUE_FLAGS = new Set([
'--url',
'--name',
'--author',
'--license',
'--source-name',
'--source-url',
'--source-type',
'--model-id',
'--dir',
'--env-file',
]);
/** 第一个"不是开关、也不是开关取值"的参数即输入文件 */
function positionalFile(argv) {
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg.startsWith('--')) {
if (VALUE_FLAGS.has(arg)) i += 1;
continue;
}
return arg;
}
return undefined;
}
const file = positionalFile(args);
const url = String(flag('--url', 'http://127.0.0.1:3000')).replace(/\/$/, '');
const writeSourceOnly = has('--write-source-only');
@@ -118,8 +143,8 @@ async function main() {
console.log(`转换说明:${model.conversionNote ?? '(无)'}`);
}
if (!model.fileSha256) {
console.error('该模型没有 fileSha256(文件缺失?),无法写入默认模型种子');
if (!model.fileSha256 && !model.sourceSha256) {
console.error('该模型没有 fileSha256 / sourceSha256(文件缺失?),无法写入默认模型种子');
process.exit(1);
}
@@ -127,10 +152,16 @@ async function main() {
id: model.id,
name: model.name,
format: model.format,
sha256: model.fileSha256,
// 主匹配键用上传压缩包的 sha256(稳定);直接上传模型文件时退回产物 sha256
sourceSha256: model.sourceSha256 ?? model.fileSha256,
fileSha256: model.fileSha256 ?? '',
convertedFrom: (model.conversionNote ?? '').split(',')[0] || null,
updatedAt: new Date().toISOString(),
};
console.log(` 种子匹配键:sourceSha256=${String(seed.sourceSha256).slice(0, 16)}…`);
if (model.sourceSha256) {
console.log(' (用的是压缩包 sha256:Assimp 转换产物每次都不一样,只有它稳定)');
}
const content = renderDefaultModelSource(seed);
if (dryRun) {