Files
aurora-admin/tools/verify-deploy-consistency.mjs
aurora-admin f1fbfc2ddb
Regression / regression (push) Canceled after 0s
feat(品牌标识): 几何 K 图标(favicon/顶栏标记/theme-color) + 并行会话成果入库
## 品牌标识(本次会话)

起因:品牌此前没有任何图形标识 —— 唯一 favicon 是内联 data-URI 里的字母「A」,
那是 v2.0.0「Aurora Admin → Kole UI」改名漏掉的一处(PC 顶栏也是「A」,
移动端站已是「K」;移动端文档站则完全没有 favicon)。

- 几何:24 网格三个互不接触的笔画(竖 + 两斜),圆头描边;
  描边 2.25 → 16px 标签页尺寸下正好 1.5px = 规范原文「描边1.5px」
- 取色分两套(刻意):favicon 硬编码品牌蓝/白(渲染在浏览器标签栏,不继承 kole-dark);
  顶栏标记走 currentColor(实测暗色下自动转 rgb(20,22,28))
- 新增 theme-color 双条(light #FFFFFF / dark #1C1F26,取 --kole-color-card-bg)
- 修 site/app.js hero 标语 KOLE ADMIN → KOLE UI(改名变形残留)
- 移动端 7 个模板补 favicon(此前计数 0)

验收:门禁 9 条全 OK(site-routing/site-routes/mobile-docs/mobile-site/isolation/
theme/nav/i18n/icons);PC 回归 1464/1464 · 移动端 807/807,各连跑 8 次一致;
两端 favicon 405 字节逐字节一致;PC 站控制台错误 1→0。

## 并行会话成果(本次一并入库)

- 图标系统:2576 图标(TDesign/Element Plus,MIT)+ 11 端注入 + 5 个构建门禁工具
  + IconPreview 预览页 + ICON-SPEC.md 冻结规格
- 移动端平台:47 组件 × 6 端 + 文档站 53 页 + 隔离门禁
- PC 组件:103 个大后台组件 / 组件11 批次
- uni-app:PC 端试点 + 移动端端实现 + 真实编译验证

## 工程

- .gitignore 补 .scratch/ 与 .zcode-preexisting-*.txt(会话中间产物,实测 9.1MB,不入库)
- CHANGELOG 补品牌标识条目
- ROADMAP 登 S8-P4(品牌标识任务包 + og:image/apple-touch-icon 未做部分)
2026-09-21 10:05:48 +08:00

107 lines
5.6 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
/**
* verify-deploy-consistency.mjs — 发布快照与本地工作树的一致性核对
*
* 用法:
* node tools/verify-deploy-consistency.mjs # 默认对公网 https://kole-ui.mymoyu.top
* node tools/verify-deploy-consistency.mjs --base=http://127.0.0.1:3311
* node tools/verify-deploy-consistency.mjs --only=site/playground # 只核对路径含该子串的文件
* 退出码:0 一致 / 1 有滞后或缺失 / 2 环境问题(连不上 / 目录缺失)
*
* 为什么需要它:AGENTS §九 要求"打包前后各查一次产物哈希",但那是**手工步骤** —— 实测踩过:
* 2026-09-20 的一次发布里,快照与工作树在 6 个文件上不一致(`site/app.js` / `site/style.css` /
* `site/dev-server.js` / `site/versions.json` / `tests/_runtime.js` / 生成物 `report.json`),
* 其中 `style.css` 的差异直接在线上表现为**缺陷**:375px 下语言切换器被挤出顶栏(实测宽度 0)。
* 逐字节比对只能靠脚本,凭"刚发布过"的印象一定会漏。
*
* 判据:按目录递归枚举工作树里**会随发布出去**的文件(与 pack-deploy 的发布集一致),
* 逐个拉远端同路径比对 sha256。生成物(tests/report*.json|xml、site/data.js)单独计数,
* 差异只提示不算失败 —— 它们本来就随每次运行变化。
*
* 注意:比对是**大小写敏感**的(远端是 Linux 容器)。本地 Windows 上写错大小写的路径
* 在本地 dev-server 能过、到线上就是 404 —— 那类问题由这里暴露,不要"顺手"改成不敏感。
*/
import { readFileSync, existsSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { collectPublishSet, NOT_WEB_SERVED } from './lib/publish-set.mjs';
const args = process.argv.slice(2);
const arg = (name) => {
const hit = args.find((a) => a.startsWith(`--${name}=`));
return hit ? hit.slice(name.length + 3) : '';
};
const BASE = (arg('base') || process.env.DEPLOY_BASE || 'https://kole-ui.mymoyu.top').replace(/\/+$/, '');
const ONLY = arg('only');
const CONC = Number(arg('conc') || process.env.DEPLOY_CONC) || 8;
const ROOT = process.cwd();
/* 生成物:本来就随每次运行变化(回归报告、precompute 后的 data.js),差异只提示不算失败 */
const GENERATED = /^(tests\/report(-junit)?\.(json|xml)|site\/data\.js)$/;
/* 发布集直接复用打包器那份实现(tools/lib/publish-set.mjs,规则真源是 .dockerignore)。
再剔掉不经 HTTP 暴露的构建/运维配置(Dockerfile / nginx.conf / index.html …)——
它们在镜像里但不在 web 根下,没法用 URL 比对。 */
let targets;
try {
targets = collectPublishSet(ROOT).kept.filter((f) => !NOT_WEB_SERVED.includes(f));
} catch (e) {
console.error(`[deploy] 环境问题:${e.message}(请在仓库根目录运行)`);
process.exit(2);
}
if (ONLY) targets = targets.filter((t) => t.includes(ONLY));
if (!targets.length) { console.error(`[deploy] --only=${ONLY} 没匹配到任何文件`); process.exit(2); }
const sha = (buf) => createHash('sha256').update(buf).digest('hex');
const hashOf = new Map(targets.filter((t) => existsSync(t)).map((t) => [t, sha(readFileSync(t))]));
/* 先确认 BASE 真的是这个站 */
try {
const probe = await fetch(`${BASE}/site/data.json`, { signal: AbortSignal.timeout(30000) });
if (!probe.ok) throw new Error(`HTTP ${probe.status}`);
} catch (e) {
console.error(`[deploy] 连不上 ${BASE}/site/data.json — ${e.message}`);
process.exit(2);
}
const lagged = [];
const missing = [];
const generatedDiff = [];
let same = 0;
/* nginx 会**故意**把这些路径 302 到别处(不是文件内容不一致):
- `/<x.y.z>/` 与 `/<x.y.z>/index.html` → 该版站点(nginx.conf 的版本裸入口规则)
- 根 `/index.html` → `/site/`
这类路径取到的字节是 nginx 的 302 响应体,与磁盘文件天然不同 —— 不排除就会报假滞后
(2026-09-20 实测:`1.4.1/index.html` 被当成「线上滞后」,而线上该 URL 本就是 302)。 */
const NGINX_REDIRECTS = [/^index\.html$/, /^\d+\.\d+\.\d+\/index\.html$/];
let skipped = 0;
const queue = [...targets].filter((rel) => {
if (NGINX_REDIRECTS.some((re) => re.test(rel))) { skipped++; return false; }
return true;
});
async function worker() {
while (queue.length) {
const rel = queue.shift();
let res;
try {
res = await fetch(`${BASE}/${encodeURI(rel)}`, { signal: AbortSignal.timeout(60000) });
} catch (e) {
lagged.push(`${rel}(请求失败 ${e.message})`);
continue;
}
if (!res.ok) { missing.push(`${rel} → HTTP ${res.status}`); continue; }
const live = sha(Buffer.from(await res.arrayBuffer()));
if (live === hashOf.get(rel)) { same++; continue; }
if (GENERATED.test(rel)) generatedDiff.push(rel);
else lagged.push(rel);
}
}
await Promise.all(Array.from({ length: CONC }, worker));
console.log(`[deploy] ${BASE}`);
console.log(`[deploy] 核对 ${targets.length - skipped} 个文件(并发 ${CONC},另跳过 ${skipped} 个 nginx 重定向路径):一致 ${same} / 滞后 ${lagged.length} / 线上缺失 ${missing.length}`);
if (generatedDiff.length) console.log(`[deploy] 生成物差异(随运行变化,不计失败):${generatedDiff.join(', ')}`);
for (const l of lagged) console.log(`[deploy] 滞后: ${l}`);
for (const m of missing) console.log(`[deploy] 缺失: ${m}`);
const bad = lagged.length + missing.length;
console.log(`\n[deploy] ${bad === 0 ? 'OK — 快照与工作树一致' : `FAIL — ${bad} 个文件不一致(线上快照落后于工作树,或工作树在打包后被改写)`}`);
process.exit(bad === 0 ? 0 : 1);