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 294866c32..8b90908b4 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.50",
+ "version": "2026-08-26.51",
"skills": [
{
"name": "agc-unity-editor",
@@ -204,7 +204,7 @@
"scripts/validate.mjs",
"scripts/vite.config.xhs-minitool.mjs"
],
- "sha256": "83f78c763407f749ffeb859843a111fe6922a77971cc84be35a6d618de4745b1"
+ "sha256": "25e302b74418c81511f95684f4752f227018d89ab1a3bf4e53a73d1516740f19"
}
]
}
diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/.selective_rule.txt b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/.selective_rule.txt
index a7d556b5a..cbfb8d2df 100644
--- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/.selective_rule.txt
+++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/.selective_rule.txt
@@ -71,3 +71,9 @@
- scripts/vite.config.xhs-minitool.mjs(原 packages/vite-config-example/vite.config.xhs-minitool.mjs)
- references/MANUAL_CHECKS.md(原 packages/validate-script/MANUAL_CHECKS.md)
- pack.mjs 的 import 改为 './validate.mjs';performance-budget.md 的门禁链接改为 '../scripts/validate.mjs';xhs/packages 已空。
+
+收尾职责归位(2026-10-05)
+- 背景:pack.mjs 之前会就地改写 dist(把 index.html 改成相对路径 / 去 module 痕迹 / 脚本移到 body 末尾 / 删空目录 / 兜底复制 icon),再打 zip。dist 只有跑完 pack 才合规,validate 看到的是中间态。
+- 决策:dist 收尾移进 `scripts/vite.config.xhs-minitool.mjs` 的 `xhsMinitoolArtifactPlugin`(`closeBundle` 钩子,`apply: 'build'` + `enforce: 'post'`)。构建结束 dist 即合规;`pack.mjs` 只做白名单预检与打 zip,不再写盘。
+- 口径:编译约束留在 `minitoolBaseConfig`,收尾只在插件里做一次,禁止在 pack 或项目脚本里重复。
+- 验证:`scripts/.vite-config.test.mjs` 真跑一遍 `vite build`,断言 dist 无 `type="module"` / `crossorigin` / 根路径、脚本在 body 末尾、空目录被清、`copyPublicDir: false` 时 icon 兜底;`scripts/.pack.test.mjs` 断言 pack 不再改写 index.html。
diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/SKILL.md b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/SKILL.md
index f9dd9a1ff..c693ee429 100644
--- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/SKILL.md
+++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/SKILL.md
@@ -8,10 +8,10 @@ metadata:
# 目标:
* 实现打包脚本 `build:xhs-minitool` 支持vite构建, validate, pack, 产出zip到固定文件: .export/xhs-minitool.zip
- 1. 复制(并按实际情况修改) `scripts/vite.config.xhs-minitool.mjs` 作为vite构建配置, 这个配置保证了产物符合小红书小工具的规范.
+ 1. 复制(并按实际情况修改) `scripts/vite.config.xhs-minitool.mjs` 作为vite构建配置, 这个配置保证了产物符合小红书小工具的规范. 构建分两段: `minitoolBaseConfig` 管编译约束, 配置内置的 `xhsMinitoolArtifactPlugin` 在 build 收尾时把 dist 整理成可直接打 zip 的形状(相对路径 / 去 module 痕迹 / 脚本移到 body 末尾 / 清空空目录 / 兜底 icon). **不要**在 pack 或别处重复这些收尾.
2. 使用 `scripts/validate.mjs` 验证上述配置构建产物目录, 这个脚本实现了一些硬性检查, 此外有一些手动检查项:
`references/manual-checks.md`
- 3. 使用 `scripts/pack.mjs` 打包成zip
+ 3. 使用 `scripts/pack.mjs` 打包成zip; 它只读 dist 打 zip, 不改写 dist
4. 跑通流程后请把需要的脚本配置复制到项目里(以免依赖skill), 并形成最终的打包脚本
* 不干扰正常的web构建
@@ -23,11 +23,12 @@ metadata:
# 脚本参数:
| 脚本 | 参数 |
|----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|
-| `scripts/vite.config.xhs-minitool.mjs` | `build.outDir`(与打包时的 `--vite-built-dir` 必须是同一个目录); `defineMinitoolConfig(overrides)`(项目已有 vite.config 时用它合并) |
+| `scripts/vite.config.xhs-minitool.mjs` | `build.outDir`(与打包时的 `--vite-built-dir` 必须是同一个目录); `defineMinitoolConfig(overrides)`(项目已有 vite.config 时用它合并); `xhsMinitoolArtifactPlugin()`(已有 vite.config 时单独加插件做收尾) |
| `scripts/validate.mjs` | `[project]`、`--json`、`--strict` |
| `scripts/pack.mjs` | `--vite-built-dir
`、`--zip-out ` |
# 一些情况:
+* 项目已有自己的 vite.config 时, 用 `defineMinitoolConfig(overrides)` 合并, 或至少把 `xhsMinitoolArtifactPlugin()` 加进 `plugins`; 漏掉插件会让 dist 保留 `type="module"` / 绝对路径, validate 会直接报错.
* 打包大小限制, 需要精简游戏内容/删减资源/压缩素材 请和用户讨论
* 如果使用了外部能力, 参考platform-abstract skill对项目先重构
* 小红书的条件可能有变化, 请以用户反馈为准, 并调整本地的构建脚本
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 83f5596c5..51f5585eb 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
@@ -1,6 +1,6 @@
#!/usr/bin/env node
import assert from 'node:assert/strict';
-import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
+import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { spawnSync } from 'node:child_process';
@@ -81,6 +81,25 @@ test('干净产物目录仍能正常打包', () => {
assert.ok(existsSync(zipOut), 'zip 未生成');
});
+test('pack 只打 zip,不再改写产物目录', () => {
+ const root = writeBuiltDir('no-rewrite');
+ // 故意留一份「构建后还没收尾」的 index.html:type=module / crossorigin / 根路径脚本。
+ const unfinalized = ''
+ + ''
+ + ''
+ + ''
+ + '';
+ writeFileSync(join(root, 'index.html'), unfinalized, 'utf8');
+
+ const zipOut = join(BASE, 'no-rewrite-out', 'xhs-minitool.zip');
+ assert.equal(packMinitool({ cwd: BASE, viteBuiltDir: 'no-rewrite', zipOut }), 0);
+ assert.equal(
+ readFileSync(join(root, 'index.html'), 'utf8'),
+ unfinalized,
+ 'pack 改写了 index.html;收尾应属于 vite.config.xhs-minitool.mjs 的插件',
+ );
+});
+
test('CLI 拒绝不认识的参数,不写出 zip', () => {
const dir = writeBuiltDir('cli');
const zipOut = join(BASE, 'cli-out.zip');
diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/.vite-config.test.mjs b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/.vite-config.test.mjs
new file mode 100644
index 000000000..ce88ab25d
--- /dev/null
+++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/.vite-config.test.mjs
@@ -0,0 +1,153 @@
+#!/usr/bin/env node
+/**
+ * vite.config.xhs-minitool.mjs 的收尾插件端到端测试:真跑一遍 `vite build`,
+ * 验证 dist 在 build 结束时已经合规(相对路径、无 module 痕迹、脚本在 body 末尾、
+ * 空目录被清掉、icon 兜底),而 pack 只负责打 zip。
+ *
+ * 这是本地开发辅助(`.` 前缀),不随 Skill Pack 发布。
+ */
+import assert from 'node:assert/strict';
+import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import test, { after } from 'node:test';
+
+import { packMinitool } from './pack.mjs';
+import { findUnsupportedFiles, validate } from './validate.mjs';
+import { defineMinitoolConfig, pruneEmptyDirs } from './vite.config.xhs-minitool.mjs';
+
+const BASE = mkdtempSync(join(tmpdir(), 'xhs-vite-config-tests-'));
+
+after(() => {
+ rmSync(BASE, { recursive: true, force: true });
+});
+
+const SOURCE_HTML = '\n'
+ + '\n'
+ + ' \n'
+ + ' \n'
+ + ' \n'
+ + ' fixture\n'
+ + ' \n'
+ + ' \n'
+ + ' \n'
+ + ' \n'
+ + ' \n'
+ + '\n';
+
+/** 造一个最小 vite 项目;返回项目根。 */
+function writeProject(name) {
+ const root = join(BASE, name);
+ mkdirSync(join(root, 'src'), { recursive: true });
+ mkdirSync(join(root, 'public', 'icons'), { recursive: true });
+ mkdirSync(join(root, 'public', 'empty-dir'), { recursive: true });
+ writeFileSync(join(root, 'index.html'), SOURCE_HTML, 'utf8');
+ writeFileSync(
+ join(root, 'src', 'main.js'),
+ "import './style.css';\ndocument.getElementById('app').textContent = 'ok';\n",
+ 'utf8',
+ );
+ writeFileSync(join(root, 'src', 'style.css'), 'body { color: red; }\n', 'utf8');
+ for (const icon of ['icon-192.svg', 'icon-512.svg']) {
+ writeFileSync(
+ join(root, 'public', 'icons', icon),
+ '\n',
+ 'utf8',
+ );
+ }
+ return root;
+}
+
+test('vite 构建收尾:dist 在 closeBundle 后即可直接打 zip', async () => {
+ const root = writeProject('finalized');
+ const { build } = await import('vite');
+ const config = defineMinitoolConfig({ root, logLevel: 'silent' });
+
+ await build({ ...config, configFile: false });
+ const outDir = join(root, 'dist-xhs-minitool');
+
+ assert.ok(existsSync(join(outDir, 'index.html')), 'index.html 未生成');
+ assert.ok(existsSync(join(outDir, 'app.js')), 'app.js 未生成');
+ assert.ok(existsSync(join(outDir, 'app.css')), 'app.css 未生成');
+
+ const html = readFileSync(join(outDir, 'index.html'), 'utf8');
+ const head = html.slice(0, html.search(/<\/head>/i));
+ assert.doesNotMatch(html, /type="module"/i, '仍残留 type="module"');
+ assert.doesNotMatch(html, /crossorigin/i, '仍残留 crossorigin');
+ assert.doesNotMatch(html, /rel="modulepreload"/i, '仍残留 modulepreload');
+ assert.doesNotMatch(html, /\b(?:src|href)="\//, '仍有根路径资源引用');
+ assert.doesNotMatch(head, /`);
- return '';
- },
- );
-
- if (scripts.length) {
- if (/<\/body>/i.test(html)) {
- html = html.replace(/<\/body>/i, ` ${scripts.join('\n ')}\n