From 87c0b2a01f0f879c7e89b025ea9192fc99780b43 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:58:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E8=84=9A=E6=9C=AC=E5=8F=AA?= =?UTF-8?q?=E5=9C=A8=20JS=20=E4=B8=8A=E5=89=A5=E6=B3=A8=E9=87=8A=E4=B8=8E?= =?UTF-8?q?=E5=AD=97=E7=AC=A6=E4=B8=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resources/agc-skills/vite-export-xhs-minitool/scripts/validate.mjs:scanCode 对 .js 用剥掉注释/字符串的副本匹配 API 名规则,避免注释里的 fetch( 误报;三条要匹配字符串参数的规则标记为扫原文;外部 URL 始终扫原文,CSS 的 url(https://…) 不再被 // 当行注释抹掉。 resources/agc-skills/vite-export-xhs-minitool/scripts/.validate.test.mjs:补注释/字符串不误报、字符串参数规则仍生效、CSS 与 JS 外部 URL 仍报错三个用例。 resources/agc-skills/manifest.json:同步 xhs 技能指纹并递增版本。 --- .../resources/agc-skills/manifest.json | 4 +- .../scripts/.validate.test.mjs | 45 +++++++++++++++++++ .../scripts/validate.mjs | 27 ++++++++--- 3 files changed, 68 insertions(+), 8 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 0168cf51c..87ba2cf8a 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.68", + "version": "2026-08-26.69", "skills": [ { "name": "agc-unity-editor", @@ -204,7 +204,7 @@ "scripts/validate.mjs", "scripts/vite.config.xhs-minitool.mjs" ], - "sha256": "016423b1f673b402d8775bad34b78e0e3166cd8021e623bc9ff103fd5d6c4dbd" + "sha256": "1c2323f06e1b278f910c9aa48ae9c3c27ec351349c0c6a485fdc0603023d27ca" } ] } 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 bd2077b40..6ef18d7b4 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 @@ -288,6 +288,51 @@ test('字符串与注释里的现代语法 / #hex / #选择器不算命中', () assert.ok(!codes(root).has('ES2018_PLUS_SYNTAX'), `actual=${[...codes(root)].sort()}`); }); +test('注释与字符串里的 API 名不算命中,但字符串参数规则仍生效', () => { + const noisy = join(BASE, 'api_noise'); + writeProject(noisy, VALID_HTML, { + ...VALID_FILES, + // `fetch(` / `XMLHttpRequest` / `new Function(` / `eval(` 只出现在注释与字符串里:不是调用。 + 'app.js': '// 不能用 fetch( 和 XMLHttpRequest 请求网络\n' + + 'var note = "new Function( 也不允许";\n' + + "var evalNote = 'eval(\\'x\\')';\n", + }); + const noisyCodes = codes(noisy); + assert.ok(!noisyCodes.has('NETWORK_FETCH'), `actual=${[...noisyCodes].sort()}`); + assert.ok(!noisyCodes.has('NETWORK_XHR'), `actual=${[...noisyCodes].sort()}`); + assert.ok(!noisyCodes.has('DYNAMIC_FUNCTION'), `actual=${[...noisyCodes].sort()}`); + assert.ok(!noisyCodes.has('DYNAMIC_EVAL'), `actual=${[...noisyCodes].sort()}`); + + const real = join(BASE, 'api_real'); + writeProject(real, VALID_HTML, { + ...VALID_FILES, + // 这条规则要匹配的就是字符串参数:剥掉字符串就会漏检。 + 'app.js': "document.querySelector('a').setAttribute('download', '');\n", + }); + const realCodes = codes(real); + assert.ok(realCodes.has('PROGRAMMATIC_DOWNLOAD'), `actual=${[...realCodes].sort()}`); +}); + +test('CSS 里的外部 URL 仍然报错(不会被当 // 注释抹掉)', () => { + const root = join(BASE, 'css_external_url'); + writeProject(root, VALID_HTML, { + ...VALID_FILES, + 'styles.css': ".hero { background: url('https://cdn.evil.example/bg.png'); }\n", + }); + assert.ok(codes(root).has('EXTERNAL_URL'), `actual=${[...codes(root)].sort()}`); +}); + +test('JS 字符串里的外部 URL 仍然报错', () => { + const root = join(BASE, 'js_external_url'); + writeProject(root, VALID_HTML, { + ...VALID_FILES, + 'app.js': "fetch('https://api.evil.example/data');\n", + }); + const actual = codes(root); + assert.ok(actual.has('NETWORK_FETCH'), `actual=${[...actual].sort()}`); + assert.ok(actual.has('EXTERNAL_URL'), `actual=${[...actual].sort()}`); +}); + test('真正的 class 私有字段仍然报错', () => { const root = join(BASE, 'private_field_real'); writeProject(root, VALID_HTML, { 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 38d7e9108..71208eeb5 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 @@ -45,6 +45,15 @@ const NAMED_ENTITIES = new Map([ ]); const ENTITY_RE = /&(?:#([0-9]+)|#x([0-9a-fA-F]+)|([a-zA-Z][a-zA-Z0-9]*));?/g; +/** + * 「代码里不该出现的 API」清单。每项是 + * `[级别, 代码, 说明, 正则, 是否必须扫原文]`。 + * + * 绝大多数规则匹配 API 名(`fetch(`、`new Function(`、`WebSocket`),必须在 JS 里先剥掉注释与 + * 字符串再扫,否则注释里写一句 `fetch(` 就会被误判。少数规则要匹配的正是字符串参数 + * (`setAttribute('download')`、`execCommand('copy')`、`addEventListener('devicemotion')`), + * 剥掉字符串会让它们彻底失效,所以标 `true` 表示这条扫原文。 + */ const CODE_PATTERNS = [ ['ERROR', 'NETWORK_FETCH', '禁止 fetch 网络请求。', /\bfetch\s*\(/g], ['ERROR', 'NETWORK_XHR', '禁止 XMLHttpRequest 网络请求。', /\bXMLHttpRequest\b/g], @@ -60,9 +69,9 @@ const CODE_PATTERNS = [ ['ERROR', 'SERVICE_WORKER', '禁止 Service Worker。', /\bserviceWorker\b/g], ['ERROR', 'SHARED_ARRAY_BUFFER', '禁止 SharedArrayBuffer 多线程。', /\bSharedArrayBuffer\b/g], ['ERROR', 'GEOLOCATION', '禁止地理定位。', /\b(?:navigator\s*\.\s*)?geolocation\b/g], - ['ERROR', 'CLIPBOARD', '禁止剪贴板 API。', /\bnavigator\s*\.\s*clipboard\b|execCommand\s*\(\s*['"](?:copy|cut|paste)['"]/g], + ['ERROR', 'CLIPBOARD', '禁止剪贴板 API。', /\bnavigator\s*\.\s*clipboard\b|execCommand\s*\(\s*['"](?:copy|cut|paste)['"]/g, true], ['ERROR', 'HARDWARE_API', '禁止蓝牙、USB、HID 或串口 API。', /\bnavigator\s*\.\s*(?:bluetooth|usb|hid|serial)\b/g], - ['ERROR', 'SENSOR_API', '禁止传感器和设备运动/朝向 API。', /\b(?:Accelerometer|Gyroscope|Magnetometer|AmbientLightSensor|DeviceMotionEvent|DeviceOrientationEvent)\b|addEventListener\s*\(\s*['"](?:devicemotion|deviceorientation)['"]/g], + ['ERROR', 'SENSOR_API', '禁止传感器和设备运动/朝向 API。', /\b(?:Accelerometer|Gyroscope|Magnetometer|AmbientLightSensor|DeviceMotionEvent|DeviceOrientationEvent)\b|addEventListener\s*\(\s*['"](?:devicemotion|deviceorientation)['"]/g, true], ['ERROR', 'SCREEN_API', '禁止屏幕共享或 requestFullscreen。', /\b(?:getDisplayMedia|(?:webkit)?[rR]equestFullscreen)\b/g], ['ERROR', 'DEVICE_INFO', '禁止电池、网络信息或媒体设备枚举。', /\b(?:getBattery|enumerateDevices)\s*\(|navigator\s*\.\s*connection\b/g], ['ERROR', 'PERSISTENT_STORAGE', '禁止请求持久化存储或跨域存储访问。', /\bnavigator\s*\.\s*storage\s*\.\s*(?:persist|persisted|getDirectory)\s*\(|\bdocument\s*\.\s*requestStorageAccess\s*\(/g], @@ -78,7 +87,7 @@ const CODE_PATTERNS = [ ['ERROR', 'BACKGROUND_SYNC', '移动端容器不支持后台同步。', /\b(?:SyncManager|PeriodicSyncManager)\b/g], ['ERROR', 'POINTER_KEYBOARD_LOCK', '移动端容器不支持指针或键盘锁定。', /\brequestPointerLock\b|\bnavigator\s*\.\s*keyboard\s*\.\s*lock\b/g], ['ERROR', 'WINDOW_MANAGEMENT', '移动端容器不支持窗口管理 API。', /\bgetScreenDetails\b/g], - ['ERROR', 'PROGRAMMATIC_DOWNLOAD', '容器禁止程序化文件下载。', /\.download\s*=|setAttribute\s*\(\s*['"]download['"]/g], + ['ERROR', 'PROGRAMMATIC_DOWNLOAD', '容器禁止程序化文件下载。', /\.download\s*=|setAttribute\s*\(\s*['"]download['"]/g, true], ['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], @@ -453,13 +462,19 @@ export function scanCssReferences(path, text, collector, baseLine = 1) { export function scanCode(path, text, collector) { const lineAt = createLineCounter(text); + const isJs = extname(path).toLowerCase() === '.js'; + // JS 先剥掉注释与字符串再扫 API 名,避免把注释里的 `fetch(` 当成真实调用;CSS / HTML 没有这层 + // 噪声可剥,原样扫。标了「必须扫原文」的规则仍用 `text`(见 CODE_PATTERNS 的说明)。 + const apiText = isJs ? stripJsNoise(text) : text; - for (const [severity, code, message, pattern] of CODE_PATTERNS) { - for (const match of text.matchAll(pattern)) { + for (const [severity, code, message, pattern, needsRawText] of CODE_PATTERNS) { + for (const match of (needsRawText === true ? text : apiText).matchAll(pattern)) { collector.add(severity, code, path, lineAt(match.index), message); } } + // 外部 URL 必须在原文上扫:真实的 `fetch('https://...')` 就在字符串里,剥掉字符串会漏检; + // 另外 `stripJsNoise` 把 `//` 当行注释,CSS 的 `url(https://...)` 会被从 `//` 起整段抹掉。 for (const match of text.matchAll(EXTERNAL_URL_RE)) { const value = match[0]; if (NONNETWORK_NAMESPACE_URLS.has(value)) continue; @@ -467,7 +482,7 @@ export function scanCode(path, text, collector) { `检测到外部 URL:${value}。所有资源和数据必须本地打包。`); } - if (extname(path).toLowerCase() === '.js') { + if (isJs) { scanJsSyntax(path, text, collector); }