feat(web): T-WEB-005 模型上传 + VRM 优先 + 版本识别

This commit is contained in:
2026-09-14 19:00:55 +08:00
parent ff8c4dd26a
commit 73e793318e
7 changed files with 517 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
import { inspectVrm } from '../src/models/vrm-inspector';
const dir = mkdtempSync(join(tmpdir(), 'opendance-vrm-'));
/** 构造合法 GLB:magic + version + length + JSON chunk */
function buildGlb(json: unknown): Buffer {
const jsonBuf = Buffer.from(JSON.stringify(json), 'utf8');
const pad = (4 - (jsonBuf.length % 4)) % 4;
const padded = Buffer.concat([jsonBuf, Buffer.alloc(pad, 0x20)]);
const header = Buffer.alloc(12);
header.write('glTF', 0, 'ascii');
header.writeUInt32LE(2, 4);
header.writeUInt32LE(12 + 8 + padded.length, 8);
const chunkHeader = Buffer.alloc(8);
chunkHeader.writeUInt32LE(padded.length, 0);
chunkHeader.writeUInt32LE(0x4e4f534a, 4);
return Buffer.concat([header, chunkHeader, padded]);
}
function write(name: string, content: Buffer): string {
const path = join(dir, name);
writeFileSync(path, content);
return path;
}
describe('VRM 版本识别', () => {
it('识别 VRM 1.0(VRMC_vrm 扩展)', async () => {
const path = write('v1.vrm', buildGlb({ extensions: { VRMC_vrm: { specVersion: '1.0' } } }));
const result = await inspectVrm(path);
expect(result.isGlb).toBe(true);
expect(result.vrmVersion).toBe('1.0');
});
it('识别 VRM 0.x(VRM 扩展)', async () => {
const path = write('v0.vrm', buildGlb({ extensions: { VRM: { specVersion: '0.0', exporterVersion: '0.79' } } }));
const result = await inspectVrm(path);
expect(result.vrmVersion).toBe('0.0');
});
it('VRM 扩展缺失时版本为 null(普通 GLB)', async () => {
const path = write('plain.glb', buildGlb({ asset: { version: '2.0' }, meshes: [] }));
const result = await inspectVrm(path);
expect(result.isGlb).toBe(true);
expect(result.vrmVersion).toBeNull();
expect(result.generator).toBeNull();
});
it('文本 glTF 不按 GLB 处理', async () => {
const path = write('scene.gltf', Buffer.from('{"asset":{"version":"2.0"}}', 'utf8'));
const result = await inspectVrm(path);
expect(result.isGlb).toBe(false);
expect(result.vrmVersion).toBeNull();
});
it('文件过小时报错', async () => {
const path = write('tiny.glb', Buffer.from('glTF', 'ascii'));
await expect(inspectVrm(path)).rejects.toThrow(/过小/);
});
it('首 chunk 非 JSON 时报错', async () => {
const header = Buffer.alloc(12);
header.write('glTF', 0, 'ascii');
header.writeUInt32LE(2, 4);
header.writeUInt32LE(32, 8);
const chunk = Buffer.alloc(8);
chunk.writeUInt32LE(4, 0);
chunk.writeUInt32LE(0x004e4942, 4); // 'BIN\0'
await expect(inspectVrm(write('bin-first.glb', Buffer.concat([header, chunk, Buffer.alloc(4)])))).rejects.toThrow(
/JSON/,
);
});
});