fix: 按 Halo 应用市场审核意见整改外部资源与隐私(v1.2.5 修订)

- 默认零第三方自动请求:移除 21 个模板的 preconnect/dns-prefetch、PrismJS+autoloader、qrcode-generator 及内置一言/IP 定位 API
- 一言与代码高亮配色改为自托管;欢迎弹窗自定义 IP 接口改为 opt-in;微信分享改为复制链接;LightGallery 按 GPLv3 使用
- README 新增「外部资源与隐私」「验证与回归测试」章节,修正无效链接并补充字体 OFL 来源;同步 docs 与修正虚构的目录树
- friends 等 12 处 #strings.substring 加空值保护;logo 由两份 1.5MB 压缩去重为单份 146KB
- 新增 pnpm audit:external 与打包前置合规审计;制品补齐 34 个 docs 文档与预览图
This commit is contained in:
nxxy335 committed 2026-11-11 14:37:09 +08:00
1 parent c0ad7331d4
commit dda86294b0
36 files changed
+830 -236

No files matched your search

+48 -3
View File
@@ -10,6 +10,7 @@ import fs from "node:fs";
import path from "node:path";
import zlib from "node:zlib";
import { fileURLToPath } from "node:url";
import { auditTemplates, auditReadmeLinks } from "./audit-external.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -131,11 +132,17 @@ const INCLUDE_ROOT = [
"settings.yaml",
"LICENSE",
"README.md",
/* 上架说明引用的预览图必须真实存在于制品中(审核指南 4.6 / 2.2) */
"WarmIsland Preview.webp",
"pnpm-lock.yaml",
"pnpm-workspace.yaml",
];
const INCLUDE_DIRS = ["templates"];
const EXCLUDE_DIRS = new Set([".git", "node_modules", "src", "dist", "compat", ".astro"]);
/* README 引用的 docs/*.md 也要随包分发,避免制品内出现死链 */
const INCLUDE_DOCS = ["docs"];
const EXCLUDE_DIRS = new Set([
".git", "node_modules", "src", "dist", "compat", ".astro", ".vitepress",
]);
/* ---------------- 构建产物前置校验 ----------------
* templates/ 被 .gitignore 忽略,克隆仓库或清理工作区后默认不存在。
@@ -173,14 +180,16 @@ for (const name of INCLUDE_ROOT) {
}
}
function walk(dir, prefix) {
function walk(dir, prefix, opts = {}) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const abs = path.join(dir, entry.name);
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
if (EXCLUDE_DIRS.has(entry.name)) continue;
walk(abs, rel);
walk(abs, rel, opts);
} else {
/* docs 只收 Markdown 源文件,不收 VitePress 构建产物 / 依赖 README */
if (opts.mdOnly && !entry.name.endsWith(".md")) continue;
entries.push({ name: rel, data: fs.readFileSync(abs), date: fs.statSync(abs).mtime });
}
}
@@ -190,6 +199,10 @@ for (const dir of INCLUDE_DIRS) {
const full = path.join(root, dir);
if (fs.existsSync(full)) walk(full, dir);
}
for (const dir of INCLUDE_DOCS) {
const full = path.join(root, dir);
if (fs.existsSync(full)) walk(full, dir, { mdOnly: true });
}
if (!entries.some((entry) => entry.name === "templates/index.html")) {
fail("打包清单中缺少 templates/index.html");
@@ -201,6 +214,38 @@ if (pkgVersion !== version) {
fail(`版本号不一致:package.json=${pkgVersion},theme.yaml=${version}`);
}
/* ---------------- 上架合规前置校验 ----------------
* 1. 主题不得出现未披露的第三方自动请求 / 远程动态加载(审核指南 1.4 / 4.4.4)
* 2. README 的相对链接必须在制品内真实存在(审核指南 4.6 / 2.2)
* 3. logo 只保留一份压缩版,避免打包两份 1.5MB 大图
*/
const templateAudit = auditTemplates(templatesDir);
for (const problem of templateAudit.problems) {
console.error(`[package] ${problem}`);
}
if (templateAudit.problems.length) {
fail("外部资源审计未通过:模板仍存在第三方远程加载 / 预连接");
}
const linkAudit = auditReadmeLinks(new Set(entries.map((entry) => entry.name)));
for (const problem of linkAudit.problems) {
console.error(`[package] ${problem}`);
}
if (linkAudit.problems.length) {
fail("README 存在指向制品中不存在文件的相对链接");
}
if (fs.existsSync(path.join(templatesDir, "logo.png"))) {
fail("templates/logo.png 仍存在:应只保留压缩后的 templates/assets/logo.png");
}
const logoFile = path.join(templatesDir, "assets", "logo.png");
if (fs.existsSync(logoFile) && fs.statSync(logoFile).size > 300 * 1024) {
fail("templates/assets/logo.png 超过 300KB,请先压缩再打包");
}
console.log(
`[package] 合规审计通过(${templateAudit.htmlCount} 个模板、${linkAudit.checked} 个相对链接、logo 单份压缩)`
);
const outFile = path.join(root, "dist", `warm-island-${version}.zip`);
buildZip(entries, outFile);