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)。
158 lines
11 KiB
JavaScript
158 lines
11 KiB
JavaScript
/* 发布脱敏(redaction)—— 构建期把「仅供仓库内部」的基础设施细节从对外产物中滤掉。
|
||
*
|
||
* 为什么需要它(实测):
|
||
* CHANGELOG.md 是仓库内部的变更流水,里面忠实记录了部署过程(服务器目录名、
|
||
* 镜像回滚标签、docker compose 命令、产物哈希、并发会话轮次)。这些内容被
|
||
* build-site.ps1 / tools/precompute.mjs / tools/build-mobile.mjs 原样注入站点,
|
||
* 而站点是**公网可下载的静态文件**(https://kole-ui.mymoyu.top)——
|
||
* 抓一次 /site/m/changelog.html 就能拿到内网地址段、服务器目录布局与部署时序。
|
||
*
|
||
* 2026-09-23 线上实证命中:/opt/aurora-admin.prev-20260920-0712、
|
||
* kole-ui-showcase:pre-20260920-0710、docker compose build、192.168.5.7。
|
||
*
|
||
* 设计原则:
|
||
* 1) **不改 CHANGELOG.md** —— 它是仓库内部真源,脱敏不能牺牲内部可追溯性。
|
||
* 过滤只发生在「写进对外产物」这一步,因此每个注入点都要过一遍 redact()。
|
||
* 2) **白名单式替换**,不做全文删除 —— 把敏感片段换成不泄露事实的等价说法,
|
||
* 保持句子可读(「按 AGENTS §九 标准流程发布」),而不是留一堆空洞。
|
||
* 3) **零依赖**,与仓库铁律 1 一致。
|
||
*
|
||
* 用法(三处注入点都必须调用):
|
||
* import { redactText, redactDeep } from './lib/redact-publish.mjs';
|
||
* writeFileSync(out, redactText(src)); // 文本产物(HTML / JSON 字符串)
|
||
* const safe = redactDeep(dataObject); // 结构化数据(data.json 的 changelog 段)
|
||
*/
|
||
'use strict';
|
||
|
||
/* 每条规则:[匹配, 替换]。顺序有意义 —— 更具体的写在前面,
|
||
避免「/opt/xxx.prev-yyyy」被更宽的规则先吃掉一半。 */
|
||
const RULES = [
|
||
/* --- 服务器目录与路径 --- */
|
||
[/\/opt\/[\w.@-]+(?:\/[\w.-]+)*/g, '部署目录'],
|
||
[/\b(?:cd|ls|mv|tar|scp|rsync)\s+\/opt\/[\w.-]+(?:\/[\w.-]+)*/g, '部署命令'],
|
||
|
||
/* --- 内网地址段(保留「有内网部署」这一事实,不保留具体网段) ---
|
||
三段式与四段式必须分开写:192.168.x.x 是 192.168 + 两段,
|
||
10.x.x.x / 172.16-31.x.x 是首段 + 三段,写在一起会多消耗一个八位组而永不匹配
|
||
(2026-09-23 自检抓到:合并写法让 192.168.5.7 漏过替换)。
|
||
CIDR 后缀 /24 一并吃掉,避免留下「内网地址/24」这种残句。 */
|
||
[/\b192\.168\.\d{1,3}\.\d{1,3}(?:\/\d{1,2})?\b/g, '内网地址'],
|
||
[/\b10\.\d{1,3}\.\d{1,3}\.\d{1,3}(?:\/\d{1,2})?\b/g, '内网地址'],
|
||
[/\b172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}(?:\/\d{1,2})?\b/g, '内网地址'],
|
||
|
||
/* --- 镜像 / 容器名(含回滚标签与摘要) --- */
|
||
[/\bkole-ui-showcase(?::[\w.-]+)?/g, '站点镜像'],
|
||
[/\bdocker\s+tag\s+[\w./:-]+\s+[\w./:-]+/gi, '镜像打标命令'],
|
||
[/\bdocker\s+compose\s+(?:build|up|down|ps|logs)\b[^\n]*/gi, '容器编排命令'],
|
||
[/\bdocker\s+(?:build|run|rmi|push|save|load)\b[^\n]*/gi, '容器命令'],
|
||
|
||
/* --- 部署主机与登录 --- */
|
||
[/\broot@[\w.-]+/g, '部署账号'],
|
||
[/\b(?:ssh|scp)\s+(?:-o\s+\S+\s+)*[\w.-]+@[\w.-]+/g, '部署传输命令'],
|
||
|
||
/* --- 隧道 / 私有域名(公网域名本身是公开的,保留;只滤掉隧道与私有源) --- */
|
||
[/\bfrpc?\b/gi, '隧道'],
|
||
|
||
/* --- 发布过程指纹:产物哈希与文件数(可供离线比对,属内部细节) ---
|
||
只吃「裸的 16 位十六进制」,且要求前后不是字母数字(避免切进 URL、ID、色值)。
|
||
文件数「1531/1531」按 context 替换:只在同时出现对账词时才算指纹。 */
|
||
[/\b(?<![0-9a-zA-Z])[0-9a-f]{16}(?![0-9a-zA-Z])/g, '产物指纹'],
|
||
[/(?:逐文件哈希对账|哈希(?:一致|对账)|树哈希(?:与本地一致)?)[^\n。;]{0,20}?\d+\s*\/\s*\d+/g, '逐文件哈希对账一致'],
|
||
/* 部署包规模:「(1531 文件 / 5.8 MB,1.0 MB gzip)」——同一个发布过程的产物指纹。 */
|
||
[/[((]\s*\d+\s*文件\s*\/\s*[\d.]+\s*MB[^))]*[))]/g, '(产物规模)'],
|
||
[/[((]\s*\d+\s*文件\s*\/\s*[\d.]+\s*MB\b[^\n。;]*/g, '(产物规模)'],
|
||
|
||
/* --- 会话 / 轮次用语(暴露开发方式,非技术事实) ---
|
||
注意匹配顺序:先长词后短词,否则「本会话」先被短规则吃掉一半。 */
|
||
[/(?:并发会话|并行会话|另一个会话|另一个子\s*agent|上一轮的两个子\s*agent|子\s*agent\s*并存)/g, '并行改动'],
|
||
[/(?:本会话|本次会话|这一轮|本轮(?!改动)|上一轮|前一批|本批次)/g, '本轮改动'],
|
||
[/(?:派子\s*agent|派活)/g, '批次派发'],
|
||
[/\/son\b/g, '批次派发'],
|
||
[/\/agi\b/g, '全自主模式'],
|
||
];
|
||
|
||
/* 关键词命中检测:只在这些词出现时才做替换,避免误伤正常文案
|
||
(例如组件演示里的「待办表格」不该被当成敏感信息处理)。 */
|
||
const TRIGGERS = [
|
||
/\/opt\//, /\b192\.168\./, /\b10\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, /\b172\.(?:1[6-9]|2\d|3[01])\./,
|
||
/kole-ui-showcase/, /\bdocker\b/i, /root@/, /\bfrpc?\b/i,
|
||
/* 会话/轮次用语必须和 RULES 里的那一组保持同步 ——
|
||
漏一个就会出现「规则写好了但永远不触发」的静默漏网(2026-09-23 实测踩到:
|
||
`**本会话直接产出的一批**` 因 TRIGGERS 缺词而整行未过滤)。 */
|
||
/并发会话|并行会话|另一个会话|本会话|本次会话|这一轮|本轮|上一轮|前一批|本批次|派子\s*agent|派活/,
|
||
/\/son\b/, /\/agi\b/,
|
||
/\b(?<![0-9a-zA-Z])[0-9a-f]{16}(?![0-9a-zA-Z])/,
|
||
/* 对账句式本身也是信号:即使没带 16 位哈希,「逐文件哈希对账 1531/1531」
|
||
照样暴露了产物规模,需要过滤掉文件数。 */
|
||
/哈希对账|树哈希|哈希一致/,
|
||
/* 部署包规模:「(1531 文件 / 5.8 MB,1.0 MB gzip)」 */
|
||
/\d+\s*文件\s*\/\s*[\d.]+\s*MB/,
|
||
];
|
||
|
||
/** 文本脱敏:命中触发词才逐条替换,否则原样返回。 */
|
||
export function redactText(s) {
|
||
if (typeof s !== 'string' || !s) return s;
|
||
if (!TRIGGERS.some((re) => re.test(s))) return s;
|
||
let out = s;
|
||
for (const [re, to] of RULES) out = out.replace(re, to);
|
||
/* 清理替换后可能出现的重复与空洞(「部署目录/部署目录」「→ →」) */
|
||
out = out.replace(/(部署目录)(?:\s*→\s*\1)+/g, '$1');
|
||
out = out.replace(/([,、])\1+/g, '$1');
|
||
/* 指纹后面的产物规模一并吃掉:「产物指纹,1531 文件」→「产物指纹」
|
||
注意 markdown 反引号可能夹在中间(`f00d…`,1531 文件),要跨过它们。 */
|
||
out = out.replace(/产物指纹(`*)[,,]\s*`*\d+\s*(?:个)?\s*文件/g, '产物指纹');
|
||
/* 文件数被吃掉后可能留下空括号?(「…对账 0 差异(1531/1531)」→「…对账一致)」)
|
||
—— 去掉紧跟在替换结果后的孤立右括号。 */
|
||
out = out.replace(/(逐文件哈希对账一致)\s*[))]/g, '$1');
|
||
/* 「`pack-deploy --tar`(1531 文件 / 5.8 MB…)」整块被换掉后,
|
||
前一个 `→` 箭头会悬空成「→ (产物规模)→」,收掉重复箭头。 */
|
||
out = out.replace(/(产物规模)\s*→\s*/g, '(产物规模)→ ');
|
||
/* 替换后被 markdown 反引号包住会留下不配对的引号:「(`产物指纹)」→「(产物指纹)」
|
||
直接把「反引号+替换词」整体规整成裸词。 */
|
||
out = out.replace(/`(产物指纹|产物规模|部署目录|内网地址|站点镜像|容器编排命令|并行改动|本轮改动|批次派发|全自主模式|逐文件哈希对账一致|容器命令|镜像打标命令|部署命令|部署账号|部署传输命令)`/g, '$1');
|
||
/* 相邻替换词造成的重复:「批次派发 批次派发推进」→「批次派发推进」
|
||
(`/son` 与「派活」是同一个动作的两种写法,替换后撞在一起)。 */
|
||
const REPL = '产物指纹|产物规模|部署目录|内网地址|站点镜像|容器编排命令|并行改动|本轮改动|批次派发|全自主模式|逐文件哈希对账一致|容器命令|镜像打标命令|部署命令|部署账号|部署传输命令';
|
||
out = out.replace(new RegExp(`(${REPL})\\s+(?=\\1)`, 'g'), '');
|
||
/* 注意:这里**不能**再有「删掉行尾替换词」的规则 ——
|
||
那会把「→ 替换 → 容器编排命令」整句砍成「→ 替换 →」,属于有损删除
|
||
(2026-09-23 实测:加了该规则后第二次调用结果与第一次不同,且丢信息)。 */
|
||
/* 收尾:替换词原来被 markdown 反引号包着时,替换后可能只剩单边引号
|
||
(「(`产物指纹)」)。紧贴替换词的反引号一律去掉 —— 替换词本身已是干净词,
|
||
不需要代码样式;而正文里真正的代码跨度(如 `pack-deploy --tar`)不与替换词相邻,不受影响。 */
|
||
out = out.replace(new RegExp('`(?=' + REPL + ')', 'g'), '');
|
||
out = out.replace(new RegExp('(?<=' + REPL + ')`', 'g'), '');
|
||
/* 括号里被整块替换后留下悬空的连接符:「(目录 + 镜像 站点镜像)」→「(站点镜像)」
|
||
「(备份目录 部署目录)」→「(部署目录)」。 */
|
||
out = out.replace(new RegExp(`[((]\\s*(?:[++、]|—+|-{1,2}|→)\\s*(?:[\\w\\u4e00-\\u9fa5-]{0,8}\\s*)?(${REPL})\\s*[))]`, 'g'), '($1)');
|
||
out = out.replace(new RegExp(`[((]\\s*(${REPL})\\s*(?:[++、]|—+|-{1,2}|→)\\s*[\\w\\u4e00-\\u9fa5-]{0,8}\\s*[))]`, 'g'), '($1)');
|
||
/* 括号内只剩连接符或空白 → 整个括号删掉。 */
|
||
out = out.replace(/[((]\s*(?:[++、]|—+|-{1,2}|→|\s)+\s*[))]/g, '');
|
||
/* 「本轮改动两个并行改动时」这类撞词(两个替换词描述同一场合)收成一句。 */
|
||
out = out.replace(/(本轮改动|并行改动)(?:两个|多个)?(本轮改动|并行改动)/g, '多个改动并行');
|
||
return out;
|
||
}
|
||
|
||
/** 结构化数据脱敏:递归遍历对象/数组的所有字符串值。返回**新对象**,不改入参。 */
|
||
export function redactDeep(v) {
|
||
if (typeof v === 'string') return redactText(v);
|
||
if (Array.isArray(v)) return v.map(redactDeep);
|
||
if (v && typeof v === 'object') {
|
||
const o = {};
|
||
for (const k of Object.keys(v)) o[k] = redactDeep(v[k]);
|
||
return o;
|
||
}
|
||
return v;
|
||
}
|
||
|
||
/** 自检:用于 verify-redaction.mjs 与门禁断言。 */
|
||
export const _selfTest = [
|
||
['部署侧(192.168.5.7)的镜像已在仓库改好', '内网地址'],
|
||
['备份目录 /opt/aurora-admin.prev-20260920-0712', '部署目录'],
|
||
['镜像 kole-ui-showcase:pre-20260920-0710 保留回滚点', '站点镜像'],
|
||
['按 AGENTS §九 发布 → docker compose build && up -d', '容器编排命令'],
|
||
['暂存目录逐文件哈希对账 0 差异(1531/1531)', '产物指纹'],
|
||
['并发会话 07:36 重建了薄壳与 app.js', '并行改动'],
|
||
['继续 /son 派活推进清单覆盖', '批次派发'],
|
||
];
|