From 230c9a2d28dba01b43a6459582b8c639d23c95cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 6 Oct 2026 21:39:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=93=E5=8C=85=E8=84=9A=E6=9C=AC=E5=85=88?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E5=90=8E=E8=90=BD=E7=9B=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vite-export-xhs-minitool/scripts/pack.mjs:抽出 encodeZip,makeZip 先把条目与字节算完并通过 index.html / 禁止条目 / 体积校验,最后才写 zip,失败时不再留半成品。 scripts/.pack.test.mjs:补测试覆盖 makeZip 内部校验失败时不留 zip。 resources/agc-skills/manifest.json:同步该 Skill 的内容指纹与 pack 版本。 --- .../resources/agc-skills/manifest.json | 4 +-- .../scripts/.pack.test.mjs | 25 +++++++++++++++ .../vite-export-xhs-minitool/scripts/pack.mjs | 32 +++++++++++++------ 3 files changed, 50 insertions(+), 11 deletions(-) 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)`, + ); } /**