Files
pangolin/web/website/scripts/check-redline.mjs
wangjia 4df8dc6f87 feat(web): 官网 ui_kits/website → Astro 纯静态 SSG 迁移 (tsk_acMYQ-Z-EIF_)
新建 web/website/(Astro 零 SSR):

- 组件迁移:交互块保留 .jsx 经 @astrojs/react(Header/AnnouncementBar/SignupForm/PricingPlans),纯展示块转 .astro 去运行时 JS;像素以原型为基准。
- 令牌同源:build-tokens.mjs 从 design/colors_and_type.css 生成 tokens.gen.css,仅剔除第三方 Google Fonts @import,数值不改。
- 字体自托管:@fontsource(Sora/Manrope/Noto Sans SC/JetBrains Mono),无第三方 CDN。
- 图标:Lucide 构建期内联 SVG(替代 unpkg CDN),零运行时、零 CDN。
- i18n:/(zh)与 /en/(en)双路由单显,语言切换组件;文案沿用 ui_kits 脱敏文案。
- 安全:public/_headers 严格 CSP(全 self + 自托管资源 + 内联片段 sha256,无 unsafe-inline)+ HSTS;无支付表单。
- CI:.gitea/workflows/website.yml 构建(红线扫描+lint+CSP 哈希)→ 同时发布 Cloudflare Pages 主站与镜像。
- 灾备:README 写明干净环境 npm ci && npm run build 可直接部署到任意静态托管;dist 指纹确定性,主站镜像一致。

测试:npm run lint(0 error)/ npm test(build+CSP 哈希注入+红线扫描 0 命中)/ 两次干净构建 dist 指纹一致。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 14:26:40 +08:00

76 lines
2.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* check-redline.mjs — 官网产物脱敏红线词扫描(CI 红线,对齐 design/CLAUDE.md §1 铁律 13)。
*
* 扫描对象:构建产物 dist/ 下所有 .html(含正文、属性、aria-label、<title>、meta)。
* 红线词:VPN(大小写敏感)、翻墙、科学上网、突破封锁、自由穿越、Go anywhere(不分大小写)。
* 例外(与仓库 ci/scan-redline.sh 一致):外部渠道 handle @PangolinVPN_bot / @pangolinvpn。
*
* 命中即非零退出(CI fail)。默认扫描 ./dist,可传参指定目录。
*/
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
import { join, resolve } from 'node:path';
const ROOT = resolve(process.argv[2] ?? 'dist');
if (!existsSync(ROOT)) {
console.error(`[redline] 找不到待扫描目录:${ROOT}(请先 npm run build`);
process.exit(2);
}
/** 大小写敏感的红线词(与 bash 版一致:VPN 仅禁大写形态,避免误伤 pangolin.vpn 域名占位) */
const CASE_SENSITIVE = ['VPN', '翻墙', '科学上网', '突破封锁', '自由穿越'];
/** 大小写不敏感 */
const CASE_INSENSITIVE = [/go anywhere/gi];
/** 允许例外的渠道 handle(扫描前先抹去,避免误报) */
const WHITELIST = [/@[Pp]angolin[Vv][Pp][Nn]_?[Bb]ot/g, /@pangolinvpn/g];
function listHtml(dir) {
const out = [];
for (const name of readdirSync(dir)) {
const p = join(dir, name);
const st = statSync(p);
if (st.isDirectory()) out.push(...listHtml(p));
else if (name.endsWith('.html')) out.push(p);
}
return out;
}
let violations = 0;
const files = listHtml(ROOT);
for (const file of files) {
let text = readFileSync(file, 'utf8');
for (const w of WHITELIST) text = text.replace(w, '');
const hits = [];
for (const word of CASE_SENSITIVE) {
let idx = text.indexOf(word);
while (idx !== -1) {
hits.push({ word, ctx: text.slice(Math.max(0, idx - 24), idx + word.length + 24).replace(/\s+/g, ' ') });
idx = text.indexOf(word, idx + word.length);
}
}
for (const re of CASE_INSENSITIVE) {
for (const m of text.matchAll(re)) {
hits.push({ word: m[0], ctx: text.slice(Math.max(0, m.index - 24), m.index + m[0].length + 24).replace(/\s+/g, ' ') });
}
}
if (hits.length) {
violations += hits.length;
console.error(`${file}`);
for (const h of hits) console.error(` 红线词 [${h.word}] …${h.ctx}`);
}
}
console.log(`→ 红线扫描完成:${files.length} 个 HTML 文件,${violations} 处命中。`);
if (violations) {
console.error('\n脱敏检查失败!禁用词:VPN、翻墙、科学上网、突破封锁、自由穿越、Go anywhere');
console.error('代用词:网络加速、极速畅连、稳定、加速线路、隐私保护、无日志');
process.exit(1);
}
console.log('✅ 脱敏扫描通过 — 未发现红线词。');