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;改为经典脚本并按依赖顺序