/* 发布脱敏(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(? 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 派活推进清单覆盖', '批次派发'], ];