校验脚本只在 JS 上剥注释与字符串

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 技能指纹并递增版本。
This commit is contained in:
2026-10-06 21:58:59 +08:00
parent a94d8c6cff
commit 87c0b2a01f
3 changed files with 68 additions and 8 deletions
@@ -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"
}
]
}
@@ -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, {
@@ -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);
}