diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json index 392096126..8d329e435 100644 --- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json @@ -1,6 +1,6 @@ { "schemaVersion": "agc-skill-pack.v1", - "version": "2026-08-26.66", + "version": "2026-08-26.67", "skills": [ { "name": "agc-unity-editor", @@ -204,7 +204,7 @@ "scripts/validate.mjs", "scripts/vite.config.xhs-minitool.mjs" ], - "sha256": "8a096ba5ac398ee45276e47d955691f834ecb0af8facadb43d52186f90458313" + "sha256": "ad9b5a6dc1b71e0413a1a71780c60d17076d13fbf67517a939c57565e6d0a477" } ] } diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/.pack.test.mjs b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/.pack.test.mjs index 8ed0e67f7..2804d7bb4 100644 --- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/.pack.test.mjs +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/.pack.test.mjs @@ -69,6 +69,31 @@ test('存在不支持文件时打包整体失败,且不删除任何文件', () assert.equal(existsSync(join(BASE, 'no-delete-out')), false, '失败时不应写出 zip'); }); +test('makeZip 的校验也发生在落盘前,失败时不留 zip', () => { + // 缺 index.html:文件扩展名都合法,预检会放行,失败点在 makeZip。 + const noIndex = writeBuiltDir('no-index'); + rmSync(join(noIndex, 'index.html')); + const noIndexOut = join(BASE, 'no-index-out', 'xhs-minitool.zip'); + assert.throws( + () => packMinitool({ cwd: BASE, viteBuiltDir: 'no-index', zipOut: noIndexOut }), + /缺少 index\.html/, + ); + assert.equal(existsSync(noIndexOut), false, '缺 index.html 时不应写出 zip'); + + // 含禁止条目:vite.config.js 扩展名在白名单里,但命中 FORBIDDEN_ENTRY。 + writeBuiltDir('forbidden-entry', { 'vite.config.js': 'export default {};\n' }); + const forbiddenOut = join(BASE, 'forbidden-entry-out', 'xhs-minitool.zip'); + assert.throws( + () => packMinitool({ + cwd: BASE, + viteBuiltDir: 'forbidden-entry', + zipOut: forbiddenOut, + }), + /禁止文件/, + ); + assert.equal(existsSync(forbiddenOut), false, '含禁止条目时不应写出 zip'); +}); + test('干净产物目录仍能正常打包', () => { writeBuiltDir('clean'); const zipOut = join(BASE, 'clean-out', 'xhs-minitool.zip'); diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/pack.mjs b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/pack.mjs index 98ebd432a..9d69fd6e8 100644 --- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/pack.mjs +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/pack.mjs @@ -150,8 +150,8 @@ function collectZipEntries(rootDir) { return entries; } -export function writeZip(rootDir, zipPath) { - const entries = collectZipEntries(rootDir); +/** 把已收集的条目编码成完整 zip 字节。只算内存,不落盘——校验没过时磁盘上不能留半成品。 */ +function encodeZip(entries) { const now = new Date(); const dosTime = ((now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1)) & 0xffff; @@ -235,8 +235,15 @@ export function writeZip(rootDir, zipPath) { eocd.writeUInt32LE(localBuf.length, 16); eocd.writeUInt16LE(0, 20); + return Buffer.concat([localBuf, centralBuf, eocd]); +} + +/** 落盘版:写 zip 并返回条目名列表(保持既有导出名,供只想要 zip 的调用方使用)。 */ +export function writeZip(rootDir, zipPath) { + const entries = collectZipEntries(rootDir); + const buffer = encodeZip(entries); rmSync(zipPath, { force: true }); - writeFileSync(zipPath, Buffer.concat([localBuf, centralBuf, eocd])); + writeFileSync(zipPath, buffer); return entries.map((entry) => entry.name); } @@ -258,20 +265,27 @@ export function auditZipFile(file, size) { } function makeZip(viteBuiltDir, zipPath) { - const entries = writeZip(viteBuiltDir, zipPath); - if (!entries.includes('index.html')) { + // 先把条目和字节都算完、校验通过,最后才落盘:中途失败时预期输出路径上不能留一份半成品, + // 更不能让宿主把半成品当成本次构建的产物。 + const entries = collectZipEntries(viteBuiltDir); + const names = entries.map((entry) => entry.name); + if (!names.includes('index.html')) { throw new Error('zip 根目录缺少 index.html(可能多套了一层目录)'); } - const bad = entries.find((entry) => FORBIDDEN_ENTRY.test(entry)); + const bad = names.find((entry) => FORBIDDEN_ENTRY.test(entry)); if (bad) { throw new Error(`zip 内含禁止文件:${bad}`); } - const size = statSync(zipPath).size; - for (const item of auditZipFile(zipPath, size)) { + const buffer = encodeZip(entries); + for (const item of auditZipFile(zipPath, buffer.length)) { if (item.severity === 'ERROR') throw new Error(item.message); console.warn(`[warn] ${item.message}`); } - console.log(`zip 已生成:${zipPath}(${entries.length} 个条目,${(size / MIB).toFixed(2)} MiB)`); + rmSync(zipPath, { force: true }); + writeFileSync(zipPath, buffer); + console.log( + `zip 已生成:${zipPath}(${entries.length} 个条目,${(buffer.length / MIB).toFixed(2)} MiB)`, + ); } /**