Regression / regression (push) Canceled after 0s
安全: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)。
137 lines
5.4 KiB
JavaScript
137 lines
5.4 KiB
JavaScript
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);
|
||
}
|