小红书导出技能:Chrome 61 能力改为硬性 ERROR,pack 自带白名单

- validate.mjs 恢复 CSS_UNSUPPORTED_PATTERNS / MODERN_RUNTIME_API,命中判 ERROR
- 删除「基线层 + 增强层」写法,references 明确 Chrome 61 为硬基线
- pack.mjs 复制白名单与遍历实现,不再 import validate.mjs
- SKILL.md 参数表补 validate / pack / vite.config 的默认值
- 修正 decision-log 中与本次相反的旧结论
This commit is contained in:
2026-10-06 18:03:00 +08:00
parent 163564a811
commit f3f52dc5e0
11 changed files with 162 additions and 172 deletions
@@ -1,6 +1,6 @@
{
"schemaVersion": "agc-skill-pack.v1",
"version": "2026-08-26.61",
"version": "2026-08-26.62",
"skills": [
{
"name": "agc-unity-editor",
@@ -204,7 +204,7 @@
"scripts/validate.mjs",
"scripts/vite.config.xhs-minitool.mjs"
],
"sha256": "9a4ebd56d9fe30dacacec35ab2af941ea08cffc3764564a6d0331bb251cfc612"
"sha256": "5c7cd2098061be1203b4055c806b0cb40f2f54efdf4efc34066c98a086a26250"
}
]
}
@@ -12,22 +12,24 @@ metadata:
1. 复制 (并按实际情况修改) `scripts/vite.config.xhs-minitool.mjs` 作为vite构建配置, 这个配置保证了产物符合小红书小工具的规范.
2. 使用 `scripts/validate.mjs` 验证上述配置构建产物目录, 这个脚本实现了一些硬性检查, 此外有一些手动检查项:
`references/manual-checks.md`
3. 使用 `scripts/pack.mjs` 打包成zip; `--zip-out` 默认 **`../.export/xhs-minitool.zip`** (假设 cwd 是 `game/` 子工程, 产物落在 **项目根** `.export/xhs-minitool.zip`; 相对 cwd 解析)
3. 使用 `scripts/pack.mjs` 打包成zip; `--zip-out` 默认 **`../.export/xhs-minitool.zip`** (假设 cwd 是 `game/` 子工程,
产物落在 **项目根** `.export/xhs-minitool.zip`; 相对 cwd 解析)
4. 跑通流程后请把需要的脚本配置复制 (agc_install_skill_resource )到项目里 (以免依赖skill), 并形成最终的打包脚本
* 不干扰正常的web构建
# 假设和默认项:
* vite项目目录: game/ ,各个工具 (包括示例vite配置)默认cwd 就是 vite 项目根, 对于非标准的目录结构需要给出显式的参数来适应
* vite项目目录: game/
* 各个工具 (包括示例vite配置) 默认cwd 就是 vite 项目根(所以对于非标准的目录结构需要给出显式的参数来适应)
# 脚本参数:
| 脚本 | 参数 |
|----------------------------------------|------------------------------------------------------------------|
| `scripts/vite.config.xhs-minitool.mjs` | `build.outDir`(与打包时的 `--vite-built-dir` 必须是同一个目录) |
| `scripts/validate.mjs` | `[project]`、`--json` |
| `scripts/pack.mjs` | `--vite-built-dir <dir>`、`--zip-out <path>` |
| 脚本 | 参数 |
|----------------------------------------|----------------------------------------------------------------------------------------------------------------|
| `scripts/vite.config.xhs-minitool.mjs` | `build.outDir`(默认 `dist-xhs-minitool`;与打包时的 `--vite-built-dir` 必须是同一个目录) |
| `scripts/validate.mjs` | `[project]`(默认 `dist-xhs-minitool`)、`--json` |
| `scripts/pack.mjs` | `--vite-built-dir <dir>`(默认 `dist-xhs-minitool`)、`--zip-out <path>`(默认 `../.export/xhs-minitool.zip`) |
# 一些情况:
@@ -39,12 +41,12 @@ metadata:
## Reference
| 文档 | 何时读 |
|-------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
| [js-api.md](references/js-api.md) | 本地 API 快照; |
| [zip-artifact-spec.md](references/zip-artifact-spec.md) | 写 HTML 文件类型、容器 CSP、`index.html` 要求 |
| [device-capabilities.md](references/device-capabilities.md) | 处理端能力时:哪些 Web 能力可用 / 不可用及替代写法、如何实现常见交互(手势、拍照、选图等) |
| [js-compatibility.md](references/js-compatibility.md) | 写 JS / 选择构建产物时:Android 8.1 出场 Chrome / WebView 61 最低基线、Web API 检测与局部降级 |
| [css-compatibility.md](references/css-compatibility.md) | 写 CSS / 选择构建产物时:Chrome 61 基线、能力检测、现代 CSS 增强与局部回退 |
| [cross-platform-h5.md](references/cross-platform-h5.md) | 适配多端时:触摸、滚动、安全区、PC 模拟器与真机差异 |
| [performance-budget.md](references/performance-budget.md) | 包体、静态数据、Base64、媒体、长列表与 WebGL 资源控制和降级 |
| 文档 | 何时读 |
|-------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| [js-api.md](references/js-api.md) | 本地 API 快照; |
| [zip-artifact-spec.md](references/zip-artifact-spec.md) | 写 HTML 文件类型、容器 CSP、`index.html` 要求 |
| [device-capabilities.md](references/device-capabilities.md) | 处理端能力时:哪些 Web 能力可用 / 不可用及替代写法、如何实现常见交互(手势、拍照、选图等) |
| [js-compatibility.md](references/js-compatibility.md) | 写 JS / 选择构建产物时:Chrome 61 硬基线、不可用 API 与替代写法 |
| [css-compatibility.md](references/css-compatibility.md) | 写 CSS / 选择构建产物时:Chrome 61 硬基线、不支持的能力与替代写法 |
| [cross-platform-h5.md](references/cross-platform-h5.md) | 适配多端时:触摸、滚动、安全区、PC 模拟器与真机差异 |
| [performance-budget.md](references/performance-budget.md) | 包体、静态数据、Base64、媒体、长列表与 WebGL 资源控制和降级 |
@@ -1,145 +1,52 @@
# CSS 兼容性规范
> 小工具 CSS 的最低兼容基线是 **Android 8.1 出场 Chrome / WebView 61**。这不等于只能写旧 CSS:交付物采用“Chrome 61 可用的基线层 + 能力检测后的增强层”,在新内核上使用更合适的 CSS 能力。
> 目标内核固定为 **Android 8.1 出场 Chrome / WebView 61**。晚于 61 的 CSS 能力一律不得出现在最终产物里:浏览器会静默忽略,`validate.mjs` 判为 `CSS_UNSUPPORTED_FEATURE`(ERROR)。
## 1. 基线原则
## 1. 硬性要求
- 基线层保证布局、文字、按钮和核心交互在 Chrome 61 可用;增强层可以使用新 CSS 改善布局、视觉或交互。
- 按功能点提供回退与增强,不维护两套完整页面或两份完整样式表,避免双份样式逐渐漂移。
- 根据特性选择检测方法:能用声明级回退的按层叠覆盖,能准确查询的使用 `@supports`,语法检测无法证明实际布局行为时使用 JS 做最小行为检测。
- 浏览器会静默丢弃无法解析的选择器、声明或整条 at-rule。不能像 JS 一样靠异常发现 CSS 不兼容,必须检查最终产物和实际布局。
- 兼容性以最终 zip 内的 CSS 为准。源码使用了构建工具,不代表产物已经兼容。
- 最终 zip 内的 CSS 必须能被 Chrome 61 完整解析;不得保留任何晚于 61 的选择器、声明或 at-rule。
- 浏览器静默丢弃无法解析的 CSS,不像 JS 会抛异常;兼容性以最终产物为准,源码用了构建工具不代表产物已兼容。
- 不用 UA、Android 版本或机型字符串决定样式能力。
## 2. Chrome 61 不可作为唯一实现的能力
## 2. Chrome 61 不支持的能力与替代写法
以下能力晚于 Chrome 61,不能作为唯一实现。先提供基线路径,再用适合该能力的检测方式启用增强。
| 能力 | 基线写法 / 降级方式 |
|-------------------------------------------------------------|----------------------------------------------------------------------------------------------|
| Flexbox `gap` / `row-gap` / `column-gap` | 基线用子项单边 `margin`;增强用 JS 实际测量 Flex 布局后切换 class,不能只检测 `gap` 属性语法 |
| `aspect-ratio` | 固定媒体尺寸,或用百分比 `padding-top` 比例盒;内容绝对定位 |
| `min()` / `max()` / `clamp()` | 先写固定值、百分比或 `calc()`,再用媒体查询分档 |
| `inset`、`margin-inline`、`padding-block` 等逻辑属性 / 简写 | 使用 `top/right/bottom/left` 与 `margin-left/right`、`padding-top/bottom` 等物理属性 |
| `overflow: clip` | 使用 `overflow: hidden` |
| `:focus-visible` | 先提供 `:focus` 样式;增强规则不能让键盘焦点在基线端消失 |
| `:has()` | 由 JS 在父元素上切换状态 class |
| Container Queries(`@container`) | 使用 viewport 媒体查询,或由 JS 按容器尺寸切换 class |
| Subgrid | 使用普通 Grid、Flex 或显式轨道尺寸 |
| CSS Nesting、Cascade Layers(`@layer`)、`@property` | 已有构建链能可靠展开时才在源码使用;最终产物不得保留为核心规则 |
| `dvh` / `svh` / `lvh` | 先用 `%` / `100vh`;受软键盘影响的全屏高度用 JS 维护 CSS 变量并保留静态兜底 |
| `color-mix()`、`oklab()`、`oklch()` 等现代颜色 | 先写 `#hex`、`rgb()`、`rgba()` 或 `hsl()` 颜色 |
| `backdrop-filter` | 先给不依赖模糊的实色 / 半透明背景;模糊仅作增强,并同时考虑 `-webkit-backdrop-filter` |
| `text-wrap: balance` 等现代排版属性 | 保留普通换行;不要让其决定关键区域高度 |
| 能力 | 必须改用的写法 |
|-------------------------------------------------------------|---------------------------------------------------------------------------------------|
| Flex `gap` / `row-gap` / `column-gap` | Flex 用子项单边 `margin`;Grid 用 `grid-gap` |
| `aspect-ratio` | 固定媒体尺寸,或用百分比 `padding-top` 比例盒;内容绝对定位 |
| `min()` / `max()` / `clamp()` | 固定值、百分比或 `calc()`;需要分档时用媒体查询 |
| `inset`、`margin-inline`、`padding-block` 等逻辑属性 / 简写 | `top/right/bottom/left` 与 `margin-left/right`、`padding-top/bottom` 等物理属性 |
| `overflow: clip` | `overflow: hidden` |
| `:focus-visible` | `:focus` |
| `:has()` | 由 JS 在父元素上切换状态 class |
| `@container` | viewport 媒体查询,或由 JS 按容器尺寸切换 class |
| `subgrid` | 普通 Grid、Flex 或显式轨道尺寸 |
| `@layer` / `@property` | 只允许构建期展开;最终产物不得保留 |
| `dvh` / `svh` / `lvh` | `%` / `100vh`;受软键盘影响的全屏高度用 JS 维护 CSS 变量并保留 `100vh` 兜底 |
| `color-mix()`、`oklab()`、`oklch()` 等现代颜色 | `#hex`、`rgb()`、`rgba()` 或 `hsl()` |
| `backdrop-filter` | 先给不依赖模糊的实色 / 半透明背景 |
| `text-wrap: balance` 等现代排版属性 | 保留普通换行;不要让其决定关键区域高度 |
## 3. 布局与前缀
Chrome 61 可使用 Flexbox、基础 Grid、媒体查询、CSS Variables、`calc()`、transform、transition 和 animation。仍须处理以下跨端差异:
- Flex 子项内有长文本、图片或滚动区域时,按方向显式设置 `min-width: 0` 或 `min-height: 0`,避免内容撑破容器。
- Flex 间距先用子项 `margin` 形成基线;需要时通过行为检测启用 `gap` 并清除 margin。动态增删子项时确认首尾间距仍正确。
- Grid 只使用基础显式 / 隐式轨道;间距使用 Chrome 61 可解析的 `grid-gap`。不要使用 Subgrid、Masonry 或依赖新语法的自动布局。
- Flex 间距用子项单边 `margin`;Grid 间距用 `grid-gap`。两者都不要用裸 `gap`。
- 需要隐藏文本时使用 `overflow: hidden; text-overflow: ellipsis; white-space: nowrap`。多行截断使用 `display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: <行数>; overflow: hidden`,并保证截断失效时页面仍可用。
- `user-select`、`appearance`、文字截断和毛玻璃等 WebKit 相关能力按需同时写 `-webkit-` 前缀与标准声明。不要机械给所有属性加前缀。
- 关键操作不能只在 `:hover` 出现;触摸端默认可见,并可用 `@media (hover: hover)` 增加鼠标悬停效果。
- 关键操作不能只在 `:hover` 出现;触摸端默认可见,鼠标悬停效果可放 `@media (hover: hover)`。
## 4. 基线层与增强层
## 4. 视口与安全区
### 声明回退和 `@supports`
- 页面宽度使用 `%`、Flex 或基础 Grid,不写死 `375px` 等单一机型宽度。
- `100vh` 在移动端地址栏、容器高度变化和软键盘出现时可能不等于可视高度。必须跟随可视高度时,由 JS 监听尺寸变化并维护 `--app-height`,同时保留 `100vh` 回退。
- 安全区规则须配合 `viewport-fit=cover`,用 `var(--safe-area-inset-*, env(safe-area-inset-*, 0px))` 组合;具体见 [cross-platform-h5.md](./cross-platform-h5.md)。
- 现代桌面浏览器或 PC 模拟器通过不等于 Chrome 61 通过。能够运行旧内核时,至少检查首屏、滚动区、弹层、表单、横竖屏 / 尺寸变化和核心交互;无法运行时在交付说明中标记“Chrome 61 CSS 兼容性未实测”。
先写 Chrome 61 可用的完整基线,再覆盖增强:
## 5. 构建链目标
```css
.panel {
background: rgba(255, 255, 255, 0.96);
}
@supports ((-webkit-backdrop-filter: blur(12px)) or (backdrop-filter: blur(12px))) {
.panel {
background: rgba(255, 255, 255, 0.72);
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
}
}
```
同一属性使用“旧值在前、新值在后”的回退顺序:
```css
.page {
min-height: 100vh;
min-height: var(--app-height, 100vh);
}
```
安全区须先保留普通值,再使用容器注入变量与 `env()`:
```css
.bottom-bar {
padding-bottom: 0;
padding-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
}
```
`@supports` 适合检测 `aspect-ratio`、`backdrop-filter`、动态视口单位等能由单个声明准确表达的能力。需要由 JS 切换 class 时,可以使用 `CSS.supports()` 检测同类声明。任何检测路径都必须保留基础样式。
### Flex gap 必须检测布局行为
`CSS.supports('gap', '1px')` 和 `@supports (gap: 1px)` 只能证明浏览器认识该属性和值,不能证明 `gap` 在 Flexbox 中生效;支持 Grid gap、但不支持 Flex gap 的内核也可能通过语法检测。需要启用 Flex gap 时,实际创建一次 Flex 容器并测量:
```js
function supportsFlexGap() {
var flex = document.createElement('div');
flex.style.position = 'absolute';
flex.style.visibility = 'hidden';
flex.style.display = 'flex';
flex.style.flexDirection = 'column';
flex.style.rowGap = '1px';
flex.appendChild(document.createElement('div'));
flex.appendChild(document.createElement('div'));
document.body.appendChild(flex);
var supported = flex.scrollHeight === 1;
flex.parentNode.removeChild(flex);
return supported;
}
if (supportsFlexGap()) {
document.documentElement.classList.add('supports-flex-gap');
}
```
对应 CSS 只维护一套组件规则,其中 margin 是基线,gap 是增强:
```css
.actions {
display: flex;
}
.actions > * + * {
margin-left: 12px;
}
.supports-flex-gap .actions {
column-gap: 12px;
}
.supports-flex-gap .actions > * + * {
margin-left: 0;
}
```
行为检测放在包内经典脚本中,并在 `document.body` 存在后、核心页面渲染前执行。检测一次即可,不要在 resize 或渲染循环中反复测量。
不要用 UA、Android 版本或机型字符串决定样式能力。能力检测决定是否启用增强,Chrome 61 基线层始终保留。
## 5. 有构建链与无构建链
### 直接交付静态文件
没有现成构建链时,不为 CSS 兼容性临时引入 PostCSS、Autoprefixer 或新的 npm 依赖;直接按 Chrome 61 基线编写。
### 项目已有构建链
将目标浏览器至少设置为:
项目已有构建链时,浏览器目标至少设置为:
```text
Chrome >= 61
@@ -148,10 +55,3 @@ ios_saf >= 18.4
- 压缩器也须使用相同浏览器目标,避免把兼容写法重新合并成 Chrome 61 无法解析的现代语法。
- 交付前检查构建后的 CSS;zip 中只保留最终静态产物,不带 source map 和构建配置。
## 6. 视口、安全区与实测
- 页面宽度使用 `%`、Flex 或基础 Grid,不写死 `375px` 等单一机型宽度。
- `100vh` 在移动端地址栏、容器高度变化和软键盘出现时可能不等于可视高度。必须跟随可视高度时,由 JS 监听尺寸变化并维护 `--app-height`,同时保留 `100vh` 回退。
- 安全区规则须配合 `viewport-fit=cover`,具体组合见 [cross-platform-h5.md](./cross-platform-h5.md)。
- 现代桌面浏览器或 PC 模拟器通过不等于 Chrome 61 通过。能够运行旧内核时,至少检查首屏、滚动区、弹层、表单、横竖屏 / 尺寸变化和核心交互;无法运行时在交付说明中标记“Chrome 61 CSS 兼容性未实测”。
@@ -1,12 +1,12 @@
# JavaScript 兼容性规范
> 小工具 JavaScript 的最低兼容基线是 **Android 8.1 出场 Chrome / WebView 61**。最终代码必须能在 Chrome 61 解析和运行;iOS 18.4+ 支持的额外能力只能用于能力检测后的增强路径。Chrome 61 完整支持 ES2017,最终代码以 ES2017 为构建目标。
> 目标内核固定为 **Android 8.1 出场 Chrome / WebView 61**。最终代码必须能在 Chrome 61 解析和运行;晚于 Chrome 61 的语法与运行时 API 一律不得出现在最终产物里,`validate.mjs` 判为 ERROR。Chrome 61 完整支持 ES2017,最终代码以 ES2017 为构建目标。
## 1. 语法基线
最终 zip 中的 JS 须兼容 Chrome 61:
- ES2018+ 语法须由构建链转译,例如对象 spread、异步迭代、可选链、空值合并、逻辑赋值、class 私有字段、static block、BigInt 字面量和 top-level await;更新的运行时 API 仍须做能力检测或提供必要的局部实现。
- ES2018+ 语法须由构建链转译,例如对象 spread、异步迭代、可选链、空值合并、逻辑赋值、class 私有字段、static block、BigInt 字面量和 top-level await;晚于 ES2017 的运行时 API 须改用基线写法。
语法不兼容会在脚本解析阶段直接失败,无法通过运行时 `if` 兜底。
@@ -30,8 +30,8 @@
## 3. 运行时 API
- ES2017 内置 API 和基础 DOM API 可直接使用。
- `String.prototype.replaceAll`、`Array.prototype.at`、`Object.hasOwn`、`structuredClone` 等更新 API 不应直接作为唯一实现路径。
- 使用非基础 Web API 前先做能力检测;不可用时隐藏增强功能、使用简单替代实现或给出清晰提示。
- `String.prototype.replaceAll`、`Array.prototype.at`、`Object.hasOwn`、`structuredClone` 等更新 API 晚于 Chrome 61,最终产物不得直接使用(`validate.mjs` 的 `MODERN_RUNTIME_API` 会拦下);用 `split()/join()`、下标、`Object.prototype.hasOwnProperty.call`、手写深拷贝等基线写法替代。
- 其余非基础 Web API 使用前做能力检测;不可用时降级为简单替代实现或给出清晰提示。
- 只补功能实际需要的小型本地 fallback,不引入整套通用 polyfill;所有代码仍须随包离线交付。
- 能力检测基于对象 / 方法是否存在,不按 UA、机型或系统版本字符串分支。
- 不为被容器明确禁止的能力添加 polyfill;能力边界仍以 [device-capabilities.md](./device-capabilities.md) 为准。
@@ -2,11 +2,11 @@
- 相机 / 麦克风由用户手势触发,并处理系统弹窗授权。
## JS 兼容
- 使用非基础 Web API 前先做能力检测;不可用时隐藏增强功能、使用简单替代实现或给出清晰提示;能力检测基于对象 / 方法是否存在,不按 UA、机型或系统版本字符串分支。
- 除 `replaceAll` / `.at` / `Object.hasOwn` / `structuredClone` 这类晚于 Chrome 61 的 API(须用基线写法替代)外,使用非基础 Web API 前先做能力检测;不可用时降级为简单替代实现或给出清晰提示;不按 UA、机型或系统版本字符串分支。
## CSS 兼容
- Chrome 61 基线层能够独立完成核心布局和交互,新内核通过能力检测启用增强层。
- Flex gap 使用布局行为检测,或仅使用 margin 基线;未把 `@supports (gap: …)` / `CSS.supports('gap', …)` 当作 Flex gap 检测。
- 最终产物只使用 Chrome 61 可解析的 CSS;`validate.mjs` 已拦下 gap、`min()/max()/clamp()`、逻辑属性、`:focus-visible`、`:has()`、`@container`、`dvh` 等,额外人工确认构建后没有残留。
- Flex 间距用子项 `margin`,Grid 用 `grid-gap`;不使用 flex `gap`。
- Flex 子项内有长文本、图片或滚动区域时,按方向显式设置 `min-width: 0` 或 `min-height: 0`。
- 关键操作不能只在 `:hover` 出现,触摸端默认可见;安全区用 `var(--safe-area-inset-*, env(safe-area-inset-*, 0px))` 组合,配合 `viewport-fit=cover`。
@@ -44,7 +44,7 @@ zip 内仅允许以下类型:
- **脚本必须是经典脚本**:只用 `<script src="./app.js">`,**不要 `type="module"`**,JS 里也不要 `import` / `export`。zip 离线加载、无目录服务,module 的相对 `import` 解析不可靠,典型症状是「页面渲染出来但 JS 完全不执行」。要拆多个 JS 文件时按依赖顺序写多个 `<script src>`,靠 `window` 命名空间协作,并避免 top-level `await`。
- **脚本须兼容目标 WebView**:直接交付的 JS 可使用 ES2017;已有构建链可使用更新语法,但最终须转译为面向 Chrome 61 的 ES2017 产物,见 [js-compatibility.md](./js-compatibility.md)。
- **样式可内联**:`<style>` 与 `style="..."` 都能用,无需外置。
- **样式须兼容目标 WebView**:使用 Chrome 61 基线层保证核心布局,并通过能力检测启用现代 CSS 增强;只做功能点级回退,不维护两套完整 CSS,见 [css-compatibility.md](./css-compatibility.md)。
- **样式须兼容目标 WebView**:只使用 Chrome 61 可解析的 CSS;`gap`、`min()/max()/clamp()`、逻辑属性等晚于 61 的能力由 `validate.mjs` 判为 ERROR,见 [css-compatibility.md](./css-compatibility.md)。
- **选图预览**:`<img src>` 配 `data:`(`FileReader.readAsDataURL`)或 `blob:`(`URL.createObjectURL`)均可显示;大图优先 `blob:` 并及时 `URL.revokeObjectURL()`。静态资源不得转成长 Base64 塞进源码,见 [performance-budget.md](./performance-budget.md)。
- 外部 CDN 一律加载不到,所有资源全部打包进小工具。
@@ -250,16 +250,30 @@ test('html template and classic-script requirements are enforced', () => {
);
});
test('现代 CSS / 运行时 API 作为增强层放行,不再报警', () => {
test('Chrome 61 不支持的 CSS 与运行时 API 直接报 ERROR', () => {
const root = join(BASE, 'modern_css');
writeProject(root, VALID_HTML, {
...VALID_FILES,
'styles.css': '.a { aspect-ratio: 1; gap: 8px; color: oklch(0.5 0.1 20); }\n',
'styles.css': '.a { display: flex; gap: 8px; width: min(100%, 20rem); color: oklch(0.5 0.1 20); }\n',
'app.js': "list.replaceAll('a', 'b');\nObject.hasOwn({}, 'x');\n",
});
const actual = codes(root);
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()}`);
const findings = validate(root).findings;
const css = findings.find((item) => item.code === 'CSS_UNSUPPORTED_FEATURE');
assert.ok(css, `missing CSS_UNSUPPORTED_FEATURE; actual=${[...codes(root)].sort()}`);
assert.equal(css.severity, 'ERROR');
const runtime = findings.find((item) => item.code === 'MODERN_RUNTIME_API');
assert.ok(runtime, `missing MODERN_RUNTIME_API; actual=${[...codes(root)].sort()}`);
assert.equal(runtime.severity, 'ERROR');
});
test('grid-gap / -webkit- 前缀写法不算 Chrome 61 不支持', () => {
const root = join(BASE, 'legacy_gap');
writeProject(root, VALID_HTML, {
...VALID_FILES,
'styles.css': '.grid { display: grid; grid-gap: 8px; }\n'
+ '.cols { -webkit-column-gap: 8px; }\n',
});
assert.ok(!codes(root).has('CSS_UNSUPPORTED_FEATURE'), `actual=${[...codes(root)].sort()}`);
});
test('字符串与注释里的现代语法 / #hex / #选择器不算命中', () => {
@@ -9,6 +9,7 @@
*/
import {
existsSync,
lstatSync,
mkdirSync,
readdirSync,
readFileSync,
@@ -17,12 +18,10 @@ import {
statSync,
writeFileSync,
} from 'node:fs';
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { deflateRawSync } from 'node:zlib';
import { findUnsupportedFiles } from './validate.mjs';
const MIB = 1024 * 1024;
const ZIP_HARD_LIMIT = 10 * MIB;
const ZIP_RECOMMENDED = 2 * MIB;
@@ -47,6 +46,48 @@ const USAGE = `用法:node pack.mjs [--vite-built-dir <dir>] [--zip-out <path>
--zip-out <path> 落点。默认 ${DEFAULT_ZIP_OUT}(假设 cwd 是 game/ 子工程,产物落在项目根的 .export/xhs-minitool.zip);
相对路径按当前工作目录解析,父目录不存在会自动建。`;
/**
* 产物白名单与遍历逻辑刻意在 pack 内自存一份:pack.mjs 要能单独复制进项目,不 import validate.mjs。
* 改动时须与 validate.mjs 的 ALLOWED_EXTENSIONS / SKIP_DIRS 同步。
*/
const ALLOWED_EXTENSIONS = new Set([
'.html', '.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.webp',
'.svg', '.woff', '.woff2', '.json',
]);
const SKIP_DIRS = new Set(['.git', 'node_modules', '.venv', 'venv', '__pycache__', '.cache']);
function* walkArtifactFiles(dir) {
for (const name of readdirSync(dir).sort()) {
const full = join(dir, name);
let stats;
try {
stats = lstatSync(full);
} catch {
continue;
}
if (stats.isSymbolicLink()) continue;
if (stats.isDirectory()) {
if (SKIP_DIRS.has(name)) continue;
yield* walkArtifactFiles(full);
} else if (stats.isFile()) {
yield full;
}
}
}
/** 列出产物目录里白名单外的文件;path 用 `/` 分隔,方便报错展示。 */
function findUnsupportedFiles(root) {
const base = resolve(root);
const unsupported = [];
for (const path of walkArtifactFiles(base)) {
const ext = extname(path).toLowerCase();
if (!ALLOWED_EXTENSIONS.has(ext)) {
unsupported.push({ path: relative(base, path).split(sep).join('/'), ext });
}
}
return unsupported;
}
/**
* 打包前预检:产物目录里出现小红书白名单之外的文件类型时整体失败,列出路径但**不改磁盘**。
*
@@ -82,6 +82,7 @@ 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],
['ERROR', 'MODERN_RUNTIME_API', '使用了 Chrome 61 不支持的运行时 API;目标内核会直接报错,必须改用基线写法。', /\bObject\s*\.\s*hasOwn\s*\(|\bstructuredClone\s*\(|\.replaceAll\s*\(|\.at\s*\(/g],
['WARNING', 'REGEX_LOOKBEHIND', '正则使用了 lookbehind;目标内核 Chrome 61 不支持,须改写。', /\(\?<[=!]/g],
];
@@ -96,6 +97,30 @@ const ES2018_SYNTAX_PATTERNS = [
[/\b\d[\d_]*n\b/g, 'BigInt 字面量'],
];
/**
* Chrome 61 不支持的 CSS 能力;目标是硬性要求,命中即 ERROR。
*
* gap / row-gap / column-gap 的裸写法都晚于 Chrome 61(Grid 66、Flex 84);唯一可用的是
* `grid-gap`(以及 `-webkit-*` 前缀写法),所以这里用负向后顾把它们排除,不做上下文区分。
*/
const CSS_UNSUPPORTED_PATTERNS = [
[/(?<![\w-])(?:row-|column-)?gap\s*:/gi, 'gap / row-gap / column-gap(Grid 用 grid-gap)'],
[/\baspect-ratio\s*:/gi, 'aspect-ratio'],
[/\b(?:clamp|min|max)\s*\(/gi, 'min()/max()/clamp()'],
[/(?:^|[;{\s])(?:inset|margin-inline|margin-block|padding-inline|padding-block|border-inline|border-block)\s*[:(]/gi, '逻辑属性 / 简写'],
[/overflow\s*:\s*clip/gi, 'overflow: clip'],
[/:focus-visible/gi, ':focus-visible'],
[/:has\s*\(/gi, ':has()'],
[/@container\b/gi, '@container'],
[/\bsubgrid\b/gi, 'subgrid'],
[/@layer\b/gi, '@layer'],
[/@property\b/gi, '@property'],
[/\b\d+(?:\.\d+)?(?:dvh|svh|lvh)\b/gi, 'dvh / svh / lvh'],
[/\b(?:color-mix|oklab|oklch)\s*\(/gi, 'color-mix() / oklab() / oklch()'],
[/backdrop-filter\s*:/gi, 'backdrop-filter'],
[/text-wrap\s*:\s*balance/gi, 'text-wrap: balance'],
];
const BASE64_RE = /data:[\w.+-]+\/[\w.+-]+;base64,([A-Za-z0-9+/=]+)/g;
/**
@@ -413,6 +438,13 @@ export function scanCssReferences(path, text, collector, baseLine = 1) {
}
checkLocalReference(value, path, collector, line, 'CSS url()');
}
for (const [pattern, label] of CSS_UNSUPPORTED_PATTERNS) {
for (const match of text.matchAll(pattern)) {
collector.add('ERROR', 'CSS_UNSUPPORTED_FEATURE', path, baseLine + lineAt(match.index) - 1,
`使用了 Chrome 61 不支持的 CSS 能力(${label});目标内核会静默忽略该声明,必须改用 Chrome 61 可解析的写法。`);
}
}
}
/* ------------------------------------------------------------------ */
@@ -36,7 +36,7 @@ import { defineConfig, mergeConfig } from 'vite';
/** JS 最低基线:Android 8.1 出厂 Chrome / WebView 61(完整支持 ES2017)。 */
export const MINITOOL_JS_TARGET = ['es2017', 'chrome61'];
/** CSS 目标内核:Chrome 61;iOS 18.4 能力只走能力检测后的增强路径。 */
/** CSS 目标内核:Chrome 61。 */
export const MINITOOL_CSS_TARGET = ['chrome61'];
/** 小红书要求包内自带的小工具 icon;publicDir 被关掉时由插件兜底复制。 */
@@ -1,13 +1,14 @@
# 决策记录
## 2026-10-06 小红书导出 validate/pack:去掉 strict 与增强层告警,扫 JS 先剥字符串/注释,zip 落点带 cwd 预警
## 2026-10-06 小红书导出 validate/pack:Chrome 61 能力按硬性 ERROR 拦下,pack 自带白名单不再共享
- 背景:真实项目首轮适配反馈三处。① `validate.mjs` 的 `/#[A-Za-z_$][\w$]*/g`(class 私有字段)直接在原始文本上匹配,把 `"#e8f4ff"`、`document.querySelector('#hit')` 误判为 ES2018 语法并报 ERROR,把排查引向「构建链没转译」。② `--strict` 让 references 明确鼓励的「Chrome 61 基线 + 能力检测后的增强层」(`CSS_MODERN_FEATURE`、`MODERN_RUNTIME_API`)也判失败,想 strict 通过只能砍增强层。③ 宿主在**项目根** `.export/xhs-minitool.zip` 找产物,而 `--zip-out` 相对 cwd 解析;npm 脚本挂在 `game/` 子工程时 `.export/...` 会落到 `game/.export/`,没有任何提示。
- 背景:真实项目适配反馈。① `validate.mjs` 的 `/#[A-Za-z_$][\w$]*/g`(class 私有字段)直接在原始文本上匹配,把 `"#e8f4ff"`、`document.querySelector('#hit')` 误判为 ES2018 语法并报 ERROR,把排查引向「构建链没转译」。② `CSS_MODERN_PATTERNS` / `MODERN_RUNTIME_API` 告警被移除后,`flex gap`(Chrome 84+)、`min()/max()/clamp()`(Chrome 79+)等晚于 Chrome 61 的写法被静默忽略却无人拦截,`references/manual-checks.md` 只有人读清单没有工具兜底。③ 宿主在**项目根** `.export/xhs-minitool.zip` 找产物,而 `--zip-out` 相对 cwd 解析;npm 脚本挂在 `game/` 子工程时 `.export/...` 会落到 `game/.export/`,没有任何提示。④ `pack.mjs` 直接 `import './validate.mjs'`,只复制 pack.mjs 会 `ERR_MODULE_NOT_FOUND`。⑤ `validate.mjs` 的 `[project]` 默认值只在源码 USAGE 里,SKILL 参数表没写默认值。
- 决策(扫描先剥噪音):`scanJsSyntax` 先过 `stripJsNoise`——把注释与字符串字面量替换成等长空白(保留换行),只对真代码跑 ESM 与 ES2018 检测;模板 `${...}` 仍按代码处理。`"#e8f4ff"`、`'#hit'`、注释里的 `obj?.c` 不再命中,真 `#count` 私有字段仍报错。
- 决策(退出码只由 ERROR 决定):删掉 `MODERN_RUNTIME_API` 与 `CSS_MODERN_PATTERNS` / `CSS_MODERN_FEATURE`,删掉 `--strict`。增强层写法交给 `references/css-compatibility.md`、`js-compatibility.md` 的能力检测要求约束,`validate.mjs` 不再把「能用但需检测」判失败。
- 决策(产物落点有确定写法与预警):`SKILL.md` 写明产物是项目根的 `.export/xhs-minitool.zip`,`game/` 子工程写 `../.export/xhs-minitool.zip`;`pack.mjs` 在 `--zip-out` 为相对路径且落点位于 `<cwd>/.export/` 时打 `[warn]`,打印 cwd 与绝对落点(`../.export/...`、绝对路径或其他相对位置不触发)。
- 影响范围:`resources/agc-skills/vite-export-xhs-minitool/{SKILL.md,scripts/{validate.mjs,pack.mjs}}`、其 `resources/agc-skills/manifest.json` 指纹(version=2026-08-26.56)、隐藏测试 `scripts/.validate.test.mjs`、`scripts/.pack.test.mjs`。宿主 `export/draft/xhs_minitool` 链路与 `xhsMinitoolInstruction.ts` 契约常量不变。
- 验证方式:`node --test scripts/.validate.test.mjs scripts/.pack.test.mjs scripts/.vite.config.xhs-minitool.test.mjs`(34 passed)、`npm run agc:skill-pack:check`、`git diff --check`、`npm run check:encoding`。
- 决策(Chrome 61 是硬性要求,不是「增强层」):恢复 `CSS_UNSUPPORTED_PATTERNS` / `MODERN_RUNTIME_API` 并判 **ERROR**(`CSS_UNSUPPORTED_FEATURE`),覆盖 `gap`/`row-gap`/`column-gap`(仅 `grid-gap` 与 `-webkit-` 前缀写法豁免)、`aspect-ratio`、`min()/max()/clamp()`、逻辑属性、`overflow: clip`、`:focus-visible`、`:has()`、`@container`、`subgrid`、`@layer`/`@property`、`dvh/svh/lvh`、现代颜色、`backdrop-filter`、`text-wrap: balance`,以及 `Object.hasOwn`/`structuredClone`/`replaceAll`/`.at`。`--strict` 保持移除:退出码只由 ERROR 决定。references 同步删除「基线层 + 增强层」与能力检测启用现代 CSS / 新 API 的写法;`flex gap` 一律用子项 `margin`。`:hover` 关键操作与安全区仍需人读清单。
- 决策(产物落点有确定写法与预警):`SKILL.md` 写明产物是项目根的 `.export/xhs-minitool.zip`;`--zip-out` 默认 `../.export/xhs-minitool.zip`(假定 cwd 是 `game/`),相对路径落点位于 `<cwd>/.export/` 时打 `[warn]`,打印 cwd 与绝对落点。
- 决策(pack 自带实现 + 参数表补默认值):`pack.mjs` 不再 `import './validate.mjs'`,把 `ALLOWED_EXTENSIONS` / `SKIP_DIRS` / 遍历与 `findUnsupportedFiles` 复制进 pack(改动时两处需同步);`SKILL.md` 参数表补 `validate.mjs [project]` 默认 `dist-xhs-minitool`、`pack.mjs --vite-built-dir` 默认 `dist-xhs-minitool`、`--zip-out` 默认 `../.export/xhs-minitool.zip`、`vite.config` `build.outDir` 默认 `dist-xhs-minitool`。
- 影响范围:`resources/agc-skills/vite-export-xhs-minitool/{SKILL.md,references/{css-compatibility.md,js-compatibility.md,manual-checks.md,zip-artifact-spec.md},scripts/{validate.mjs,pack.mjs,vite.config.xhs-minitool.mjs}}`、`resources/agc-skills/manifest.json` 指纹(version=2026-08-26.62)、隐藏测试 `scripts/.validate.test.mjs`、`scripts/.pack.test.mjs`。
- 验证方式:`node --test scripts/.validate.test.mjs scripts/.pack.test.mjs scripts/.vite.config.xhs-minitool.test.mjs`(36 passed)、`npm run agc:skill-pack:check`、`git diff --check`、`npm run check:encoding`。
- 边界:「web 与小工具共用同一 `game/index.html`」是默认姿势,不需要另起 root;只有入口 HTML 确实不同才需要并存 recipe,本次未写,留待需要时补。
## 2026-10-06 新增 agc_install_skill_resource:Skill 附件由宿主直接落盘,不走模型正文