import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' const root = new URL('../', import.meta.url) const connectorUrl = process.env.SPINE_CONNECTOR_URL || 'http://127.0.0.1:27843' const origin = process.env.SPINE_CONNECTOR_TEST_ORIGIN || 'http://127.0.0.1:5173' const imagePath = new URL('public/images/star.png', root) const healthResponse = await fetch(`${connectorUrl}/v1/health`, { headers: { Origin: origin } }) if (!healthResponse.ok) throw new Error(`连接器健康检查失败:HTTP ${healthResponse.status}`) const health = await healthResponse.json() if (!health.authorized) throw new Error('测试来源没有获得连接器授权') if (!health.spineFound) throw new Error('连接器没有找到本机 Spine') const skeletonJson = JSON.stringify({ skeleton: { spine: '4.2', images: './images/', fps: 30 }, bones: [{ name: 'root' }, { name: 'particle', parent: 'root' }], slots: [{ name: 'particle', bone: 'particle', attachment: 'star' }], skins: [{ name: 'default', attachments: { particle: { star: { path: 'star', width: 64, height: 64 } } } }], animations: { animation: {} }, }) const image = await readFile(imagePath) const payload = { schemaVersion: 1, projectName: 'Spine粒子连接器测试', spineVersion: JSON.parse(skeletonJson).skeleton.spine, skeletonJson, images: [{ path: 'star.png', dataBase64: image.toString('base64') }], } const response = await fetch(`${connectorUrl}/v1/convert`, { method: 'POST', headers: { Origin: origin, 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }) if (!response.ok) throw new Error(`转换失败:${await response.text()}`) const temp = await mkdtemp(join(tmpdir(), 'spine-connector-verify-')) const output = join(temp, 'SpineParticleConnectorTest.zip') try { const archive = Buffer.from(await response.arrayBuffer()) await writeFile(output, archive) const info = await stat(output) if (!info.isFile() || info.size === 0 || archive.readUInt32LE(0) !== 0x04034b50) throw new Error('连接器返回的不是有效 ZIP') const entries = [] let offset = 0 while (offset + 30 <= archive.length && archive.readUInt32LE(offset) === 0x04034b50) { const size = archive.readUInt32LE(offset + 18) const nameLength = archive.readUInt16LE(offset + 26) const extraLength = archive.readUInt16LE(offset + 28) const name = archive.subarray(offset + 30, offset + 30 + nameLength).toString('utf8') entries.push({ name, size }) offset += 30 + nameLength + extraLength + size } if (!entries.some((entry) => entry.name.endsWith('.spine') && entry.size > 0)) throw new Error('ZIP 中缺少 .spine 工程') if (!entries.some((entry) => entry.name === 'images/star.png' && entry.size === image.length)) throw new Error('ZIP 中缺少 images/star.png') console.log(JSON.stringify({ connectorVersion: health.connectorVersion, requestedSpineVersion: payload.spineVersion, actualSpineVersion: response.headers.get('x-spine-editor-version'), encodedFileName: response.headers.get('x-spine-file-name'), imageCount: payload.images.length, archiveBytes: info.size, entries, }, null, 2)) } finally { await rm(temp, { recursive: true, force: true }) }