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 b6d8d33f7..274dbbccd 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.55",
+ "version": "2026-08-26.56",
"skills": [
{
"name": "agc-unity-editor",
@@ -204,7 +204,7 @@
"scripts/validate.mjs",
"scripts/vite.config.xhs-minitool.mjs"
],
- "sha256": "bcb8910018b5d98c17e616cd154146c08b0e5cb0d3c80d0b9e3635a711ffb6e6"
+ "sha256": "19dd6324e2b5c5a40b7b2b8b2589189dc96bc9784047fc363ac5643d020965b6"
}
]
}
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 2b0fc51be..f91fc82df 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
@@ -11,7 +11,7 @@ metadata:
1. 复制(并按实际情况修改) `scripts/vite.config.xhs-minitool.mjs` 作为vite构建配置, 这个配置保证了产物符合小红书小工具的规范.
2. 使用 `scripts/validate.mjs` 验证上述配置构建产物目录, 这个脚本实现了一些硬性检查, 此外有一些手动检查项:
`references/manual-checks.md`
- 3. 使用 `scripts/pack.mjs` 打包成zip
+ 3. 使用 `scripts/pack.mjs` 打包成zip; 产物必须是**项目根**的 `.export/xhs-minitool.zip`, 而 `--zip-out` 相对 cwd 解析: npm 脚本挂在 `game/` 子工程时写 `../.export/xhs-minitool.zip`
4. 跑通流程后请把需要的脚本配置复制( agc_install_skill_resource )到项目里(以免依赖skill), 并形成最终的打包脚本
* 不干扰正常的web构建
@@ -24,8 +24,8 @@ metadata:
| 脚本 | 参数 |
|----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|
| `scripts/vite.config.xhs-minitool.mjs` | `build.outDir`(与打包时的 `--vite-built-dir` 必须是同一个目录); `defineMinitoolConfig(overrides)`(项目已有 vite.config 时用它合并) |
-| `scripts/validate.mjs` | `[project]`、`--json`、`--strict` |
-| `scripts/pack.mjs` | `--vite-built-dir
`、`--zip-out ` |
+| `scripts/validate.mjs` | `[project]`、`--json` |
+| `scripts/pack.mjs` | `--vite-built-dir `、`--zip-out `(相对 cwd 解析;宿主在项目根 `.export/xhs-minitool.zip` 找产物) |
# 一些情况:
* 打包大小限制, 需要精简游戏内容/删减资源/压缩素材 请和用户讨论
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 51f5585eb..d6af2ef9d 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
@@ -100,6 +100,30 @@ test('pack 只打 zip,不再改写产物目录', () => {
);
});
+test('--zip-out 落在 cwd 的 .export 下时给出 workdir 警告', () => {
+ writeBuiltDir('workdir');
+ const warnings = [];
+ const original = console.warn;
+ console.warn = (line) => warnings.push(String(line));
+ try {
+ // 可疑:相对 cwd 解析成 /.export/...;cwd 若是 game/ 子工程就落错地方。
+ packMinitool({ cwd: BASE, viteBuiltDir: 'workdir', zipOut: '.export/xhs-minitool.zip' });
+ // 别的相对落点(不落在 cwd 的 .export 下)不提醒。
+ packMinitool({
+ cwd: BASE,
+ viteBuiltDir: 'workdir',
+ zipOut: join('other', 'xhs-minitool.zip'),
+ });
+ } finally {
+ console.warn = original;
+ }
+ assert.equal(
+ warnings.filter((line) => line.includes('--zip-out')).length,
+ 1,
+ warnings.join('\n'),
+ );
+});
+
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/.validate.test.mjs b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/.validate.test.mjs
index cadd10726..baf799dd0 100644
--- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/.validate.test.mjs
+++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/.validate.test.mjs
@@ -250,7 +250,7 @@ test('html template and classic-script requirements are enforced', () => {
);
});
-test('modern CSS and runtime APIs are flagged as warnings', () => {
+test('现代 CSS / 运行时 API 作为增强层放行,不再报警', () => {
const root = join(BASE, 'modern_css');
writeProject(root, VALID_HTML, {
...VALID_FILES,
@@ -258,8 +258,29 @@ test('modern CSS and runtime APIs are flagged as warnings', () => {
'app.js': "list.replaceAll('a', 'b');\nObject.hasOwn({}, 'x');\n",
});
const actual = codes(root);
- assert.ok(actual.has('CSS_MODERN_FEATURE'), `missing CSS_MODERN_FEATURE; actual=${[...actual].sort()}`);
- assert.ok(actual.has('MODERN_RUNTIME_API'), `missing MODERN_RUNTIME_API; actual=${[...actual].sort()}`);
+ assert.ok(!actual.has('CSS_MODERN_FEATURE'), `unexpected CSS_MODERN_FEATURE; actual=${[...actual].sort()}`);
+ assert.ok(!actual.has('MODERN_RUNTIME_API'), `unexpected MODERN_RUNTIME_API; actual=${[...actual].sort()}`);
+});
+
+test('字符串与注释里的现代语法 / #hex / #选择器不算命中', () => {
+ const root = join(BASE, 'js_noise');
+ writeProject(root, VALID_HTML, {
+ ...VALID_FILES,
+ 'app.js': '// obj?.c ?? 2\n'
+ + 'var color = "#e8f4ff";\n'
+ + "var hit = document.querySelector('#hit');\n"
+ + '// import.meta 也只出现在注释里\n',
+ });
+ assert.ok(!codes(root).has('ES2018_PLUS_SYNTAX'), `actual=${[...codes(root)].sort()}`);
+});
+
+test('真正的 class 私有字段仍然报错', () => {
+ const root = join(BASE, 'private_field_real');
+ writeProject(root, VALID_HTML, {
+ ...VALID_FILES,
+ 'app.js': 'class Counter { #count = 0; }\n',
+ });
+ assert.ok(codes(root).has('ES2018_PLUS_SYNTAX'), `actual=${[...codes(root)].sort()}`);
});
test('oversized inline base64 is warned', () => {
@@ -299,7 +320,7 @@ test('symlinks are rejected', (t) => {
assert.ok(codes(root).has('SYMLINK'));
});
-test('CLI exit codes: pass, strict warning and error', () => {
+test('CLI exit codes: pass, warning and error', () => {
const validRoot = join(BASE, 'valid_cli');
writeProject(validRoot, VALID_HTML, VALID_FILES);
const validCli = runCli(validRoot, '--json');
@@ -311,8 +332,8 @@ test('CLI exit codes: pass, strict warning and error', () => {
writeProject(warningRoot, ''
+ ''
+ '
');
+ // 有 WARNING 也照样通过:退出码只由 ERROR 决定,没有 --strict 这种「警告即失败」的模式。
assert.equal(runCli(warningRoot).status, 0);
- assert.equal(runCli(warningRoot, '--strict').status, 2);
const errorRoot = join(BASE, 'error_cli');
writeProject(errorRoot, '');
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 61a1d942d..611f58365 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
@@ -17,7 +17,7 @@ import {
statSync,
writeFileSync,
} from 'node:fs';
-import { basename, dirname, join, relative, resolve, sep } from 'node:path';
+import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { deflateRawSync } from 'node:zlib';
@@ -226,6 +226,23 @@ function makeZip(viteBuiltDir, zipPath) {
console.log(`zip 已生成:${zipPath}(${entries.length} 个条目,${(size / MIB).toFixed(2)} MiB)`);
}
+/**
+ * cwd 歧义预警:宿主在**项目根** `.export/xhs-minitool.zip` 找产物,而 `--zip-out` 相对 cwd 解析。
+ * npm 脚本挂在 `game/` 子工程时 cwd 就是 `game/`,写 `.export/...` 会落到 `game/.export/...`,
+ * 导出判失败却没有任何提示。这里只在落点位于「当前 cwd 的 `.export/`」时提醒(`../.export/...`
+ * 或绝对路径不触发),把 cwd 与绝对落点打出来,让 workdir 问题不再静默。
+ */
+function warnIfWorkdirRelative(cwd, zipPath, zipOut) {
+ if (typeof zipOut !== 'string' || isAbsolute(zipOut)) return;
+ const relativeOut = relative(cwd, zipPath);
+ if (relativeOut.startsWith('..') || !relativeOut.startsWith(`.export${sep}`)) return;
+ console.warn(
+ `[warn] --zip-out 相对 cwd 解析:cwd=${cwd},落点=${zipPath}。`
+ + '宿主在项目根 `.export/xhs-minitool.zip` 找产物;若 cwd 是 game/ 子工程,'
+ + '请写 `../.export/xhs-minitool.zip`,否则会落在 game/.export/ 导致导出判失败。',
+ );
+}
+
/**
* @param {{
* cwd?: string,
@@ -241,6 +258,8 @@ export function packMinitool(options = {}) {
const viteBuiltDir = resolve(cwd, options.viteBuiltDir || 'dist-xhs-minitool');
const zipPath = resolve(cwd, options.zipOut);
+ warnIfWorkdirRelative(cwd, zipPath, options.zipOut);
+
if (!existsSync(viteBuiltDir)) {
throw new Error(`缺少 vite 构建产物目录 ${viteBuiltDir}`);
}
diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/validate.mjs b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/validate.mjs
index dcd50f979..48f38caec 100755
--- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/validate.mjs
+++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/validate.mjs
@@ -82,7 +82,6 @@ const CODE_PATTERNS = [
['WARNING', 'DYNAMIC_IMPORT', '动态 import 可能产生运行时资源加载;确认目标为包内静态资源。', /\bimport\s*\(/g],
['WARNING', 'COOKIE_REVIEW', 'Cookie 只能作为本地存储;不要用于服务端登录态或鉴权。', /\bdocument\s*\.\s*cookie\b/g],
['WARNING', 'NAVIGATION_REVIEW', '检测到页面导航 API;确认不会打开外链、新窗口或其他小工具。', /\blocation\s*\.\s*href\s*=(?!=)|\b(?:location\s*\.\s*(?:assign|replace)|history\s*\.\s*(?:pushState|replaceState))\s*\(/g],
- ['WARNING', 'MODERN_RUNTIME_API', '使用了较新的运行时 API;须做能力检测或局部 fallback,不能作为唯一实现路径。', /\bObject\s*\.\s*hasOwn\s*\(|\bstructuredClone\s*\(|\.replaceAll\s*\(|\.at\s*\(/g],
['WARNING', 'REGEX_LOOKBEHIND', '正则使用了 lookbehind;目标内核 Chrome 61 不支持,须改写。', /\(\?<[=!]/g],
];
@@ -97,36 +96,85 @@ const ES2018_SYNTAX_PATTERNS = [
[/\b\d[\d_]*n\b/g, 'BigInt 字面量'],
];
-/** 晚于 Chrome 61 的 CSS 能力;只能作为能力检测后的增强层,不能作为唯一实现。 */
-const CSS_MODERN_PATTERNS = [
- [/\baspect-ratio\s*:/gi, 'aspect-ratio'],
- [/(? {
+ if (chars[index] !== '\n' && chars[index] !== '\r') chars[index] = ' ';
+ };
+ const n = text.length;
+ let i = 0;
+
+ while (i < n) {
+ const ch = text[i];
+ const next = text[i + 1];
+
+ if (ch === '/' && next === '/') {
+ while (i < n && text[i] !== '\n') { blank(i); i += 1; }
+ continue;
+ }
+ if (ch === '/' && next === '*') {
+ blank(i); blank(i + 1); i += 2;
+ while (i < n && !(text[i] === '*' && text[i + 1] === '/')) { blank(i); i += 1; }
+ if (i < n) { blank(i); blank(i + 1); i += 2; }
+ continue;
+ }
+ if (ch === "'" || ch === '"') {
+ const quote = ch;
+ blank(i); i += 1;
+ while (i < n && text[i] !== quote) {
+ if (text[i] === '\\') { blank(i); i += 1; if (i < n) { blank(i); i += 1; } continue; }
+ blank(i); i += 1;
+ }
+ if (i < n) { blank(i); i += 1; }
+ continue;
+ }
+ if (ch === '`') {
+ blank(i); i += 1;
+ while (i < n) {
+ if (text[i] === '\\') { blank(i); i += 1; if (i < n) { blank(i); i += 1; } continue; }
+ if (text[i] === '`') { blank(i); i += 1; break; }
+ if (text[i] === '$' && text[i + 1] === '{') {
+ // 进入插值:里面的内容是真代码,保留;按花括号配平跳到闭合的 `}`。
+ i += 2;
+ let depth = 1;
+ while (i < n && depth > 0) {
+ if (text[i] === '{') depth += 1;
+ else if (text[i] === '}') depth -= 1;
+ if (depth === 0) break;
+ i += 1;
+ }
+ i += 1;
+ continue;
+ }
+ blank(i); i += 1;
+ }
+ continue;
+ }
+ i += 1;
+ }
+
+ return chars.join('');
+}
+
/** JS 语法基线:ESM 语句直接判 ERROR,晚于 ES2017 的语法判 WARNING。 */
export function scanJsSyntax(path, text, collector) {
const lineAt = createLineCounter(text);
- for (const match of text.matchAll(/^[ \t]*(?:import|export)\b/gm)) {
+ // 只在剥掉字符串 / 注释后的代码上匹配,避免 `"#e8f4ff"`、`'#hit'` 这类字面量误判。
+ const code = stripJsNoise(text);
+ for (const match of code.matchAll(/^[ \t]*(?:import|export)\b/gm)) {
collector.add('ERROR', 'ESM_SYNTAX', path, lineAt(match.index),
'最终包不得使用 ES Module 的 import / export;改为经典脚本并按依赖顺序