Files
aurora-admin b59247231a
Regression / regression (push) Canceled after 0s
feat(安全+品牌): 对外产物发布脱敏(3注入点/2465文件0泄露) + Kole Cup 定稿
安全:CHANGELOG.md 是仓库内部变更流水(含服务器目录、镜像回滚标签、内网网段、
部署时序、AI 工作流用语),此前被 build-site.ps1 / precompute.mjs / build-mobile.mjs
原样注入站点,而站点是公网可下载的静态文件 —— 抓一次 /site/m/changelog.html
即可拿到内网地址段与服务器目录布局。

- 新增 tools/lib/redact-publish.mjs(零依赖,构建期过滤,不改 CHANGELOG.md,
  内部可追溯性完整保留)
- 接入 precompute.mjs(PC data.json/changelog.json)与 build-mobile.mjs
  (移动端更新日志页);build-site.ps1 不碰(ASCII-only 铁律)
- 只处理 changelog 字段:components[].sources 是规范实现源码,逐字保真
  (详情页代码区主动高亮注释,剥注释会破坏该功能)
- 实测消除:/opt/aurora-admin.prev-*、kole-ui-showcase:pre-*、docker compose、
  192.168.5.7、16 位产物指纹、1531/1531、并发会话/本会话/派子 agent
- settings.html 演示占位 IP 192.168.5.0/24(= 真实网段)改为 RFC 5737 的 192.0.2.0/24

品牌:Kole Cup 饮料杯标记定稿(几何 K → 圆角杯盖 + 杯身负空间 K,无吸管),
brand-mark.json 升 schemaVersion 3(paths 支持 { d, evenodd }),
verify:brand 增至 25 条(新增 B9b:负空间必须带 fill-rule)。

验证:发布集 2465 文件全量扫描 0 泄露;PC 回归 100%(1464/1464) ·
移动端 100%(807/807);门禁品牌 25 / 隔离 31 / 移动文档 12 / 示例 9 / 版本 40 / i18n 17 全绿;
已按 AGENTS §九 发布公网,2461/2461 逐字节一致,五项验收全过。

已知未处理(既有缺口,ROADMAP S7-P28 已记录):data.mobile.json 的
meta.generated 为墙上时钟,会让 CI 的「生成物可复现」断言在重跑构建后永远非空;
该 CI 流水线本身亦从未通过(无 runner)。
2026-09-23 07:31:06 +08:00

137 lines
5.4 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.
import fs from 'node:fs';
import path from 'node:path';
export const BRAND_SPEC_REL = '.design_library/kole-ui/brand/brand-mark.json';
export const BRAND_DATA_REL = 'site/brand-mark.generated.json';
export const BRAND_ASSET_REL = 'site/assets/kole-mark.svg';
export const BRAND_MONO_REL = 'site/assets/kole-mark-mono.svg';
export const BRAND_SCHEMA_VERSION = 3;
export function readBrandSpec(root) {
const file = path.join(root, BRAND_SPEC_REL);
return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
}
/** 路径项归一化:允许写成字符串(简化)或对象 { d, evenodd }(负空间需要 evenodd)。 */
function normalizePaths(raw) {
if (!Array.isArray(raw)) return [];
return raw.map((item) => {
if (typeof item === 'string') return { d: item, evenodd: false };
if (item && typeof item.d === 'string') return { d: item.d, evenodd: !!item.evenodd };
return { d: '', evenodd: false };
});
}
export function validateBrandSpec(spec) {
const errors = [];
if (!spec || spec.schemaVersion !== BRAND_SCHEMA_VERSION) {
errors.push(`schemaVersion 必须为 ${BRAND_SCHEMA_VERSION}`);
}
if (!spec || spec.viewBox !== '0 0 24 24') errors.push('viewBox 必须为 0 0 24 24');
const g = spec?.geometry || {};
const paths = normalizePaths(g.paths);
if (!paths.length || paths.some((p) => !p.d.trim())) {
errors.push('geometry.paths 必须包含至少一条非空路径(字符串或 { d, evenodd })');
}
if (g.mode !== 'fill' && g.mode !== 'stroke') errors.push('geometry.mode 必须为 fill 或 stroke');
if (g.mode === 'stroke') {
if (g.strokeWidth !== 1.5) errors.push('stroke 模式的 geometry.strokeWidth 必须为 1.5');
if (g.linecap !== 'round') errors.push('stroke 模式的 geometry.linecap 必须为 round');
if (g.linejoin !== 'round') errors.push('stroke 模式的 geometry.linejoin 必须为 round');
}
if (g.opacities !== undefined) {
if (!Array.isArray(g.opacities) || g.opacities.length !== paths.length) {
errors.push('geometry.opacities 必须与 paths 等长');
} else if (g.opacities.some((v) => typeof v !== 'number' || !(v > 0) || v > 1)) {
errors.push('geometry.opacities 取值必须在 (0, 1] 区间');
}
}
if (!/^#[0-9A-Fa-f]{6}$/.test(spec?.favicon?.background || '')) errors.push('favicon.background 必须为六位十六进制颜色');
if (!/^#[0-9A-Fa-f]{6}$/.test(spec?.favicon?.foreground || '')) errors.push('favicon.foreground 必须为六位十六进制颜色');
if (spec?.favicon?.radius !== 6) errors.push('favicon.radius 必须为 6');
for (const [key, value] of Object.entries(spec?.sizes || {})) {
if (!Number.isFinite(value) || value <= 0) errors.push(`sizes.${key} 必须为正数`);
}
return errors;
}
/** 逐路径生成 <path>:负空间路径必须带 fill-rule="evenodd",否则挖空不会生效。 */
function pathsMarkup(spec) {
const { mode, opacities } = spec.geometry;
return normalizePaths(spec.geometry.paths)
.map((p, i) => {
const opacity = mode === 'fill' && opacities && opacities[i] != null ? ` fill-opacity="${opacities[i]}"` : '';
const rule = p.evenodd ? ' fill-rule="evenodd"' : '';
return `<path d="${p.d}"${rule}${opacity}/>`;
})
.join('');
}
/** 图形的公共外层:实心走 fill,描边走 stroke;颜色由调用方决定。 */
function groupMarkup(spec, color, { mono }) {
const { mode } = spec.geometry;
const strokeAttrs =
`fill="none" stroke="${mono ? '#000000' : color}" stroke-width="${spec.geometry.strokeWidth}"` +
` stroke-linecap="${spec.geometry.linecap}" stroke-linejoin="${spec.geometry.linejoin}"`;
return mode === 'stroke'
? `<g ${strokeAttrs}>${pathsMarkup(spec)}</g>`
: `<g fill="${mono ? '#000000' : color}">${pathsMarkup(spec)}</g>`;
}
export function renderFaviconSvg(spec) {
return [
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${spec.viewBox}">`,
`<rect width="24" height="24" rx="${spec.favicon.radius}" fill="${spec.favicon.background}"/>`,
groupMarkup(spec, spec.favicon.foreground, { mono: false }),
'</svg>',
].join('');
}
export function renderMonoSvg(spec) {
return [
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${spec.viewBox}">`,
groupMarkup(spec, '#000000', { mono: true }),
'</svg>',
].join('');
}
export function generatedData(spec) {
return {
schemaVersion: spec.schemaVersion,
name: spec.name,
viewBox: spec.viewBox,
geometry: {
mode: spec.geometry.mode,
/* 归一化后再写出:字符串项补成 { d, evenodd } 对象,保证下游拿到的结构稳定一致。 */
paths: normalizePaths(spec.geometry.paths),
...(spec.geometry.opacities ? { opacities: spec.geometry.opacities } : {}),
...(spec.geometry.mode === 'stroke'
? {
strokeWidth: spec.geometry.strokeWidth,
linecap: spec.geometry.linecap,
linejoin: spec.geometry.linejoin,
}
: {}),
},
favicon: {
background: spec.favicon.background,
foreground: spec.favicon.foreground,
radius: spec.favicon.radius,
},
sizes: spec.sizes,
assets: {
favicon: 'assets/kole-mark.svg',
mono: 'assets/kole-mark-mono.svg',
},
};
}
export function atomicWrite(file, content) {
fs.mkdirSync(path.dirname(file), { recursive: true });
const tmp = `${file}.tmp-${process.pid}`;
fs.writeFileSync(tmp, content, 'utf8');
fs.renameSync(tmp, file);
}